Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Alternatives to JPA 2026

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Alternatives to JPA 2026

Alternatives to JPA (Async/Non-Blocking)

- R2DBC
- Hibernate Reactive
- Vertx SqlClient
- Exposed R2DBC

Avatar for Sunghyouk Bae (Debop)

Sunghyouk Bae (Debop)

March 26, 2026
Tweet

More Decks by Sunghyouk Bae (Debop)

Other Decks in Programming

Transcript

  1. Agenda Alternatives to JPA • What is ORM • Pros/Cons

    of JPA • (Async/Non-Blocking) • R2DBC • Hibernate Reactive • Vert.x Sql Client • Kotlin Exposed • Virtual Threads (JDK 21+) Generated by ChatGPT
  2. ORM (Object Relational Mapping) • Object Graph vs Relational Data

    • Business Logic ਸ OOP ۽ ಴അ, Not Relational Data • Hibernate ݾ಴ : ੹ా੸ੋ SQL ޙ੄ 95%ܳ ؀୓ • DB Vendors ী ޖҙೠ ௏٘ ࢤࢿ оמ (Solution স҅) • ORM ਷ ؀۝ ౟ې೗, data centric ࠙ঠীח ੸೤ೞ૑ ঋ਺
  3. JPA ੢੼ • Java Object Graph & OOP ী ૘઺

    • Application ࢸ҅ী ૘઺ೡ ࣻ ੓਺ • Entity ٜ੄ ҙ҅ա ઁড ઑѤী ؀೧ ࣘࢿਵ۽ ಴അ • ౠ੿ DB Vendor੄ ౠࢿਸ ঌ ೙ਃ হ਺ • SQL ҳޙਸ ߓ਎ ೙ਃ হ਺ (?) • HQL, JPQL, QueryDSL ਸ ؀न ߓਕঠ ೣ • Stateful (default), Stateless Session ૑ਗ
  4. JPA ױ੼ • SQL ਸ ੉޷ ੜ উ׮ݶ, JPA ߓ਋ӝо

    ؊ য۰਎ ࣻ ੓׮ • ױࣽ SQL ੘ࢿ੉ ইצ JPA۽ Domain ਸ ࢸ҅ೞח Ѫਸ णٙೞӝ ൨ٜ׮ • stateful ജ҃ীࢲ fetch by id ח ࢿמ੉ જ૑ ঋ׮ (Stateless Session, 2nd level Cache) • DB Bulk, Set operations ী ੸೤ೞ૑ ঋ׮ • Session੉ ThreadContext۽ ઁೠػ׮ (Low throughput) -> Virtual Threads !!!) • JPA Vendor ੄ ࢚ࣁ ӝמਸ ҕࠗ೧ঠ ೠ׮ (Hibernate, Eclipse Link …) • HQL, @DynamicXXXX, @LazyCollection … • 2nd Cache, StatelessSession …
  5. JPA ࢎਊ അട • JPA о ઱ܨо ػ द੼ী ѐߊ੗о

    ػ ٜ࠙਷ ઁҕػ ӝמ݅ ࢎਊ • spring-data-jpa ӝળਵ۽݅ ࢎਊ • ূ౭౭ী hashCode, equals, toString ੤੿੄о হ਺ • Relation ࢸ੿੉ ׮নೞ૑ ঋ਺ • Poor performance -> “JPA is bad” ۄҊ য়ੋೡ ৈ૑о ݆਺ • ੸೤ೞ૑ ঋח ா੉झী ࢎਊ (ࠂ੟ೠ View, ా҅, Window ೣࣻ, Bulk ੘স) • ؀উ੉ ੓਺ীب ࢎਊೞ૑ ঋ਺ • Stateless Session, 2nd cache, Blaze Persistence ੄ View …
  6. JPA Code Review - Checklist • Base Entity ܳ ੿੄ೞחо?

    • hashCode, equals, toString ਸ ੤੿੄ ೞחо? • Hibernate о ઁҕೞח annotationਸ ੸ ੺൤ ࢎਊೞחо? • Business Identity ܳ ਤ೧ @NatualId ܳ ࢎਊೞחо? • @DynamicXXXX, @ExtraLazyCollection • SELECT N+1 ޙઁܳ ഥೖೞחо? • Entity ੿੄о ࢿמਸ Ҋ۰ೞ৓ա? • @MapsId, FetchType ١ • QueryDSL ਸ ੜ ࢎਊೞחо? • SubQuery ١ ࠂ੟ೠ ௪ܻ, Covering Indexing • Blaze Persistence ܳ ࢎਊೞחо? • fetchCount, CTE ١ QueryDSL ীࢲ ઁҕೞ૑ ঋח ӝמ • ؀۝ ੘স द, Batch ܳ ࢎਊೞѢա StatelessSession ਸ ࢎਊೞחо?
  7. ജ҃ ߸ച Async/Non-Blocking • High throughput ೙ਃࢿ੉ ֫ই૗ • Async/Non-Blocking

    ജ҃੉ ೙ਃ (JDBC ח زӝߑध) • NoSQL ਸ ઁ؀۽ ഝਊೡ ࣻ ੓ח case ח ઁೠؽ (਍৔ ҃೷) • ৈ۞о૑ ؀উٜ • IO-Bounded • R2DBC, Exposed with R2DBC 1.0+, Hibernate-Reactive, Vert.x Sql Client • CPU-Bounded • Exposed Jdbc (Coroutines, Virtual Threads), JPA (Virtual Threads)
  8. R2DBC ઱ਃ ౠ૚ • Async/Non-Blocking • Reactive Stream • Low

    resources • Back pressure ܳ ాೠ ؘ੉ఠ ൒ܴਸ ઁয • ૑ਗೞח Driver • Oracle, SQL Server, MariaDB, MySQL, Postgres, H2
  9. R2DBC vs JDBC Spring Framework ӝળ JdbcRepository (Spring Data Jdbc)

    JdbcTemplate (Spring JDBC) JDBC API JDBC Driver (Blocking) DB JDBC blocking DB-access ReactiveR2dbcRepository DatabaseClient R2DBC SPI Reactive Driver (Non-Blocking) DB R2DBC non-blocking DB-access
  10. ReactiveCrudRepository @Table("customer") data class Customer( val firstname: String, val lastname:

    String, @Id var id: Long? = null, ): Serializable { val hasId: Boolean get() = id != null } interface CustomerRepository: CoroutineCrudRepository<Customer, Long> @Test fun `execute annotated query`() = runTest { val dave = Customer("Dave", "Matthews") val carter = Customer("Carter", "Beauford") insertCustomers(dave, carter) val customer = customerRepository.findByLastname("Matthews").first() customer.shouldNotBeNull() shouldBeEqualTo dave } private suspend fun insertCustomers(vararg customers: Customer) { customerRepository.saveAll(customers.toList()).collect() }
  11. R2dbcEntityOperations @Table("posts") data class Post( @Column("title") val title: String? =

    null, @Column("content") val content: String? = null, @Id val id: Long? = null, ): Serializable { val hasId: Boolean get() = id != null } @Table("comments") data class Comment( @Column("content") val content: String? = null, @Column("post_id") val postId: Long? = null, @Id val id: Long? = null, ): Serializable { val hasId: Boolean get() = id != null } @Repository class PostRepository( private val client: DatabaseClient, private val operations: R2dbcEntityOperations, private val mappingR2dbcConverter: MappingR2dbcConverter, ) { companion object: KLoggingChannel() suspend fun count(): Long = operations.countAllSuspending<Post>() fun findAll(): Flow<Post> = operations.selectAllSuspending<Post>() suspend fun findOneById(id: Long): Post = operations.findOneByIdSuspending(id) suspend fun findOneByIdOrNull(id: Long): Post? = operations.findOneByIdOrNullSuspending(id) suspend fun findFirstById(id: Long): Post = operations.findFirstByIdSuspending(id) suspend fun findFirstByIdOrNull(id: Long): Post? = operations.findFirstByIdOrNullSuspending(id) suspend fun deleteAll(): Long = operations.deleteAllSuspending<Post>() suspend fun save(post: Post): Post = operations.insertSuspending(post) }
  12. R2DBC ೠ҅ • ࠂ੟ೠ ௪ܻח Raw Query ਵ۽ ੘স೧ঠ ೣ

    • MyBatis Dynamic SQL ١੄ DSL ੄ ب਑ਵ۽ оמ -> Ҷ੉ ??? • ׮নೠ Relation ী ؀ೠ ࠽٘ܳ ࣻ੘সਵ۽ ࣻ೯೧ঠ ೠ׮
  13. Hibernate Reactive • Hibernate ӝמਸ Reactive ߑधਵ۽ ઁҕೞӝ ਤೠ ۄ੉࠳۞ܻ

    • ূ౭౭ ੿੄ח ӝઓ JPA/Hibernate ৬ э਺ • Async/Non-Blocking ҳഅ ߑध • Reactive ߑध - Mutiny ࢎਊ (೟ण ೙ਃ) • CompletionStage ߑध -> Virtual Threads ߑधਵ۽ ߸҃ оמ • Vert.x Sql Client ܳ Database Driver ۽ ࢎਊ • Quarkusо ҕध ૑ਗ (Spring ޷૑ਗ) - RedHat ࢑ೞ • Using Hibernate Reactive, Hibernate Reactive with Panache
  14. One To Many Relation Team - Member @Entity class Team:

    AbstractValueObject() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L protected set var name: String = "" @OneToMany(mappedBy = "team", orphanRemoval = false) val members: MutableList<Member> = mutableListOf() } @Entity class Member: AbstractValueObject() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L protected set var name: String = "" var age: Int? = null /** * NOTE: ManyToOne 의 FetchType을 LAZY 로 하면 Thread 범위를 벗어나 예외가 발생한다. * NOTE: 이럴 땐 LEFT JOIN FETCH 를 수행하던가 @FetchProfile 을 사용해야 한다 */ @ManyToOne(optional = false, fetch = FetchType.EAGER) var team: Team? = null fun changeTeam(team: Team?) { this.team?.removeMember(this) team?.addMember(this) } }
  15. Query by JPQL Book - Author suspend fun findAllByMemberName(session: Session,

    name: String): List<Team> { val cb = sf.criteriaBuilder val criteria = cb.createQuery(Team::class.java) val root = criteria.from(Team::class.java) val members = root.join(Team_.members) criteria.select(root) .where(cb.equal(members.get(Member_.name), name)) return session.createQuery(criteria).resultList.awaitSuspending().apply { // 팀이 여러 개일 때, 동시에 진행할 수 있도록 한다. asFlow() .flatMapMerge { team -> session.fetch(team.members).awaitSuspending().asFlow() } .collect() } }
  16. Query by JPQL Book - Author suspend fun findAllByMemberName(session: Session,

    name: String): List<Team> { val cb = sf.criteriaBuilder val criteria = cb.createQuery(Team::class.java) val root = criteria.from(Team::class.java) val members = root.join(Team_.members) criteria.select(root) .where(cb.equal(members.get(Member_.name), name)) return session.createQuery(criteria).resultList.awaitSuspending().apply { // 팀이 여러 개일 때, 동시에 진행할 수 있도록 한다. asFlow() .flatMapMerge { team -> session.fetch(team.members).awaitSuspending().asFlow() } .collect() } } select t1_0.id, t1_0.name from Team t1_0 join Member m1_0 on t1_0.id=m1_0.team_id where m1_0.name=$1
  17. Vert.x Sql Client • True IO-Bounded Async/Non-Blocking Library (Use Netty)

    • Hibernate-Reactive use vertx-sql-client • Vertx SqlTemplate - Spring JdbcTemplate җ ਬࢎ • Cons • Raw SQL Statement ݅ ૑ਗ, No Typesafe DSL • JSON ١ ౠࣻ ࣻഋী ؀ೠ ୊ܻо ࠛউ੿ೣ
  18. Using SqlTemplate val pool = vertx.getPostgresPool() try { vertx.testWithSuspendTransaction(testContext, pool)

    { conn -> val insertStmt = "INSERT INTO customers (id, first_name, last_name) VALUES (#{id}, #{firstName}, #{lastName});" val customer = Customer( id = 4, firstName = "Iron", lastName = "Man" ) val result = SqlTemplate .forUpdate(pool, insertStmt) .mapFrom(tupleMapperOfRecord<Customer>()) .execute(customer) .coAwait() result.rowCount() shouldBeEqualTo 1 } } finally { pool.close().coAwait() }
  19. Using SqlTemplate val pool = vertx.getPostgresPool() try { vertx.testWithSuspendTransaction(testContext, pool)

    { conn -> val insertStmt = "INSERT INTO customers (id, first_name, last_name) VALUES (#{id}, #{firstName}, #{lastName});" val customer = Customer( id = 4, firstName = "Iron", lastName = "Man" ) val result = SqlTemplate .forUpdate(pool, insertStmt) .mapFrom(tupleMapperOfRecord<Customer>()) .execute(customer) .coAwait() result.rowCount() shouldBeEqualTo 1 } } finally { pool.close().coAwait() } val pool = vertx.getPostgresPool() try { vertx.testWithSuspendTransaction(testContext, pool) { conn -> val query = "SELECT * FROM customers WHERE id = #{ID}" val parameters = mapOf("ID" to 1) val rowSet = SqlTemplate.forQuery(pool, query).execute(parameters).coAwait() val customers = rowSet.map { row -> CustomerRowMapper.map(row) } customers.single().id shouldBeEqualTo 1L } } finally { pool.close().coAwait() }
  20. Vert.x Sql Client with Sql Mappers For IO-Bounded Async/Non-Blocking MyBatis

    Dynamic SQL Kotlin Exposed Statements Vert.x SQL Client RDBMS ResultSet Query Statement Builders Hibernate Reactive Entity
  21. Schema De fi nitions By Mybatis Dynamic Sql class PersonTable:

    AliasableSqlTable<PersonTable>("Person", PersonSchema::PersonTable) { val id = column<Int>("id") val firstName = column<String>("first_name") val lastName = column<String>("last_name") val birthDate = column<LocalDate>("birth_date") val employed = column<Boolean>("employed") val occupation = column<String>("occupation") val addressId = column<Int>("address_id") } class AddressTable: AliasableSqlTable<AddressTable>("Address", PersonSchema::AddressTable) { val id = column<Int>(name = "address_id") val streetAddress = column<String>(name = "street_address") val city = column<String>(name = "city") val state = column<String>(name = "state") }
  22. Execute by Vert.x Sql Client With MyBatis Dynamic SQL vertx.testWithRollbackSuspending(testContext,

    pool) { conn: SqlConnection -> val selectProvider = select(person.allColumns()) { from(person) where { person.id isNotIn { selectDistinct(person.id) { from(person) where { person.lastName isEqualTo "Rubble" } } } } }.renderForVertx() selectProvider.selectStatement shouldBeEqualTo "select * from Person " + "where id not in (select distinct id from Person where last_name = #{p1})" val persons = conn.selectList(selectProvider, PersonMapper) persons.forEach { log.debug { it } } persons shouldHaveSize 3 persons.map { it.id } shouldBeEqualTo listOf(1, 2, 3) }
  23. Execute by Vert.x Sql Client With MyBatis Dynamic SQL vertx.testWithRollbackSuspending(testContext,

    pool) { conn: SqlConnection -> val selectProvider = select(person.allColumns()) { from(person) where { person.id isNotIn { selectDistinct(person.id) { from(person) where { person.lastName isEqualTo "Rubble" } } } } }.renderForVertx() selectProvider.selectStatement shouldBeEqualTo "select * from Person " + "where id not in (select distinct id from Person where last_name = #{p1})" val persons = conn.selectList(selectProvider, PersonMapper) persons.forEach { log.debug { it } } persons shouldHaveSize 3 persons.map { it.id } shouldBeEqualTo listOf(1, 2, 3) } Build SQL Statement by MyBatis Dynamic SQL
  24. Execute by Vert.x Sql Client With MyBatis Dynamic SQL vertx.testWithRollbackSuspending(testContext,

    pool) { conn: SqlConnection -> val selectProvider = select(person.allColumns()) { from(person) where { person.id isNotIn { selectDistinct(person.id) { from(person) where { person.lastName isEqualTo "Rubble" } } } } }.renderForVertx() selectProvider.selectStatement shouldBeEqualTo "select * from Person " + "where id not in (select distinct id from Person where last_name = #{p1})" val persons = conn.selectList(selectProvider, PersonMapper) persons.forEach { log.debug { it } } persons shouldHaveSize 3 persons.map { it.id } shouldBeEqualTo listOf(1, 2, 3) } Build SQL Statement by MyBatis Dynamic SQL Execute SQL Statement by Vert.x Sql Client
  25. Exposed - Kotlin SQL Framework • High level SQL DSL

    • Lightweight ORM • Provide two layers of data access • Typesafe SQL wrapping DSL • Lightweight Data Access Object • Support Coroutines in JDBC (CPU-Bounded Async/Non-Blocking) • Use JDBC with Virtual Threads • Support R2DBC (IO-Bounded Async/Non-Blocking)
  26. Exposed - SQL DSL De fi ne Table & Relations

    object ActorInMovieTable: Table("actors_in_movies") { val movieId: Column<EntityID<Long>> = reference("movie_id", MovieTable, onDelete = ReferenceOption.CASCADE) val actorId: Column<EntityID<Long>> = reference("actor_id", ActorTable, onDelete = ReferenceOption.CASCADE) override val primaryKey = PrimaryKey(movieId, actorId) } object MovieTable: LongIdTable("movies") { val name: Column<String> = varchar("name", 255) val producerName: Column<String> = varchar("producer_name", 255) val releaseDate: Column<LocalDateTime> = datetime("release_date") } object ActorTable: LongIdTable("actors") { val firstName: Column<String> = varchar("first_name", 255) val lastName: Column<String> = varchar("last_name", 255) val birthday: Column<LocalDate?> = date("birthday").nullable() }
  27. Exposed SQL DSL - select/where fun searchActors(params: Map<String, String?>): List<ActorRecord>

    { val query: Query = ActorTable.selectAll() params.forEach { (key, value) -> when (key) { ActorTable::id.name -> value?.let { parseLongParam(key, it) }?.let { query.andWhere { ActorTable.id eq it } } ActorTable::firstName.name -> value?.let { query.andWhere { ActorTable.firstName eq it } } ActorTable::lastName.name -> value?.let { query.andWhere { ActorTable.lastName eq it } } ActorTable::birthday.name -> value?.let { parseLocalDateParam(key, it) }?.let { query.andWhere { ActorTable.birthday eq it } } } } return query.map { it.toActorRecord() } } fun findById(actorId: Long): ActorRecord? { log.debug { "Find Actor by id. id: $actorId" } return ActorTable.selectAll() .where { ActorTable.id eq actorId } .firstOrNull() ?.toActorRecord() // Entity로 조회하는 방법 // ActorEntity.findById(actorId)?.toActorRecord() }
  28. Exposed SQL DSL - batchInsert ActorTable.batchInsert(actors) { this[ActorTable.firstName] = it.firstName

    this[ActorTable.lastName] = it.lastName it.birthday?.let { birthDay -> this[ActorTable.birthday] = LocalDate.parse(birthDay) } } MovieTable.batchInsert(movies) { this[MovieTable.name] = it.name this[MovieTable.producerName] = it.producerName this[MovieTable.releaseDate] = LocalDate.parse(it.releaseDate) } movies.forEach { movie -> val movieId = MovieTable .select(MovieTable.id) .where { MovieTable.name eq movie.name } .first()[MovieTable.id] val actorIds = movie.actors.map { actor -> ActorTable .select(ActorTable.id) .where { (ActorTable.firstName eq actor.firstName) and (ActorTable.lastName eq actor.lastName) } .first()[ActorTable.id] } val movieActorIds = actorIds.map { movieId to it } ActorInMovieTable.batchInsert(movieActorIds) { this[ActorInMovieTable.movieId] = it.first.value this[ActorInMovieTable.actorId] = it.second.value } }
  29. Exposed SQL DSL Query by Join - One-to-Many @Transactional(readOnly =

    true) fun getAllMoviesWithActors(): List<MovieWithActorRecord> { log.debug { "Get all movies with actors." } val join = table.innerJoin(ActorInMovieTable).innerJoin(ActorTable) return join .select( MovieTable.id, MovieTable.name, MovieTable.producerName, MovieTable.releaseDate, ActorTable.id, ActorTable.firstName, ActorTable.lastName, ActorTable.birthday ) .groupBy { it[MovieTable.id] } .map { (_, rows) -> val movie = rows.first().toMovieRecord() val actor = rows.map { it.toActorRecord() } movie.toMovieWithActorRecord(actor) } }
  30. Exposed SQL DSL Query by Join - One-to-Many @Transactional(readOnly =

    true) fun getAllMoviesWithActors(): List<MovieWithActorRecord> { log.debug { "Get all movies with actors." } val join = table.innerJoin(ActorInMovieTable).innerJoin(ActorTable) return join .select( MovieTable.id, MovieTable.name, MovieTable.producerName, MovieTable.releaseDate, ActorTable.id, ActorTable.firstName, ActorTable.lastName, ActorTable.birthday ) .groupBy { it[MovieTable.id] } .map { (_, rows) -> val movie = rows.first().toMovieRecord() val actor = rows.map { it.toActorRecord() } movie.toMovieWithActorRecord(actor) } } SELECT MOVIES.ID, MOVIES."name", MOVIES.PRODUCER_NAME, MOVIES.RELEASE_DATE, ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.BIRTHDAY FROM MOVIES INNER JOIN ACTORS_IN_MOVIES ON MOVIES.ID = ACTORS_IN_MOVIES.MOVIE_ID INNER JOIN ACTORS ON ACTORS.ID = ACTORS_IN_MOVIES.ACTOR_ID
  31. Exposed SQL DSL With Coroutines @GetMapping("/{id}") suspend fun getMovieById( @PathVariable("id")

    movieId: Long, ): MovieRecord? = newSuspendedTransaction(readOnly = true) { movieRepository.findById(movieId)?.toMovieRecord() } suspend fun findById(movieId: Long): MovieEntity? { log.debug { "Find Movie by id. id: $movieId" } return MovieTable .selectAll() .where { MovieTable.id eq movieId } .firstOrNull() ?.let { MovieEntity.wrapRow(it) } }
  32. Exposed SQL DSL In Virtual Threads @Test open fun `get

    all actors in multiple virtual threads`() { StructuredTaskScopeTester() .rounds(Runtime.getRuntime().availableProcessors() * 8) .add { transaction { val actors = ActorTable.selectAll().map { it.toActorRecord() } actors.shouldNotBeEmpty() } } .run() @RepeatedTest(REPEAT_SIZE) open fun `get all actors`() { virtualFuture { transaction { val actors = ActorTable.selectAll().map { it.toActorRecord() } actors.shouldNotBeEmpty() } }.await() }
  33. Exposed SQL DSL In Virtual Threads @Test open fun `get

    all actors in multiple virtual threads`() { StructuredTaskScopeTester() .rounds(Runtime.getRuntime().availableProcessors() * 8) .add { transaction { val actors = ActorTable.selectAll().map { it.toActorRecord() } actors.shouldNotBeEmpty() } } .run() @RepeatedTest(REPEAT_SIZE) open fun `get all actors`() { virtualFuture { transaction { val actors = ActorTable.selectAll().map { it.toActorRecord() } actors.shouldNotBeEmpty() } }.await() } 2026-03-26 15:22:18.435 DEBUG 75749 [ Test worker] i.b.c.v.StructuredTaskScopes : Discovered StructuredTaskScopeProvider: jdk21-structured-task-scope (priority: 21) 2026-03-26 15:22:18.435 DEBUG 75749 [ Test worker] b.c.v.j.Jdk21StructuredTaskScopeProvider : ݽٚ subtask о ৮ܐؼ ٸө૑ ӝ׮݀פ׮... 2026-03-26 15:22:18.441 DEBUG 75749 [ virtual-60] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.BIRTHDAY FROM ACTORS 2026-03-26 15:22:18.441 DEBUG 75749 [ virtual-58] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.BIRTHDAY FROM ACTORS 2026-03-26 15:22:18.441 DEBUG 75749 [ virtual-62] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.BIRTHDAY FROM ACTORS 2026-03-26 15:22:18.441 DEBUG 75749 [ virtual-56] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.BIRTHDAY FROM ACTORS
  34. Exposed - DAO Lightweight ORM class BankAccount(id: EntityID<Int>): IntEntity(id) {

    companion object: IntEntityClass<BankAccount>(BankAccountTable) var number: String by BankAccountTable.number // many to many with via val owners: SizedIterable<AccountOwner> by AccountOwner via OwnerAccountMapTable }
  35. Exposed - DAO Lightweight ORM class BankAccount(id: EntityID<Int>): IntEntity(id) {

    companion object: IntEntityClass<BankAccount>(BankAccountTable) var number: String by BankAccountTable.number // many to many with via val owners: SizedIterable<AccountOwner> by AccountOwner via OwnerAccountMapTable } class AccountOwner(id: EntityID<Int>): IntEntity(id) { companion object: IntEntityClass<AccountOwner>(AccountOwnerTable) var ssn: String by AccountOwnerTable.ssn // many to many with via val accounts: SizedIterable<BankAccount> by BankAccount via OwnerAccountMapTable }
  36. Exposed DAO Many-to-many - with() & load() val ownersWithAccounts =

    AccountOwner.all().with(AccountOwner::accounts).toList() var loadedOwner1 = AccountOwner.findById(account1.id)!!.load(AccountOwner::accounts)
  37. Exposed DAO Many-to-many - with() & load() val ownersWithAccounts =

    AccountOwner.all().with(AccountOwner::accounts).toList() -- Postgres SELECT account_owner.id, account_owner.ssn FROM account_owner SELECT bank_account.id, bank_account."number", owner_account_map.owner_id, owner_account_map.account_id FROM bank_account INNER JOIN owner_account_map ON owner_account_map.account_id = bank_account.id WHERE owner_account_map.owner_id IN (1, 2) var loadedOwner1 = AccountOwner.findById(account1.id)!!.load(AccountOwner::accounts)
  38. Exposed DAO Many-to-many - with() & load() val ownersWithAccounts =

    AccountOwner.all().with(AccountOwner::accounts).toList() -- Postgres SELECT account_owner.id, account_owner.ssn FROM account_owner SELECT bank_account.id, bank_account."number", owner_account_map.owner_id, owner_account_map.account_id FROM bank_account INNER JOIN owner_account_map ON owner_account_map.account_id = bank_account.id WHERE owner_account_map.owner_id IN (1, 2) var loadedOwner1 = AccountOwner.findById(account1.id)!!.load(AccountOwner::accounts) -- Postgres SELECT account_owner.id, account_owner.ssn FROM account_owner WHERE account_owner.id = 1; SELECT bank_account.id, bank_account."number", owner_account_map.owner_id, owner_account_map.account_id FROM bank_account INNER JOIN owner_account_map ON owner_account_map.account_id = bank_account.id WHERE owner_account_map.owner_id = 1;
  39. Exposed DAO Jdbc Transaction with Virtual Threads fun <T> newVirtualThreadJdbcTransaction(

    executor: ExecutorService? = VirtualThreadExecutor, db: Database? = null, transactionIsolation: Int? = null, readOnly: Boolean = false, statement: JdbcTransaction.() -> T, ): T = virtualThreadJdbcTransactionAsync( executor = executor, db = db, transactionIsolation = transactionIsolation, readOnly = readOnly, statement = statement ).await() fun <T> virtualThreadJdbcTransactionAsync( executor: ExecutorService? = VirtualThreadExecutor, db: Database? = null, transactionIsolation: Int? = null, readOnly: Boolean = false, statement: JdbcTransaction.() -> T, ): VirtualFuture<T> { val effectiveExecutor = executor ?: Executors.newVirtualThreadPerTaskExecutor() require(!effectiveExecutor.isShutdown && ! effectiveExecutor.isTerminated) { "ExecutorService is already shutdown." } return virtualFuture(executor = effectiveExecutor) { val isolationLevel = transactionIsolation ?: db?.transactionManager?.defaultIsolationLevel transaction(db = db, transactionIsolation = isolationLevel, readOnly = readOnly) { statement(this) } } }
  40. Exposed DAO Auditable Table & Entity interface Auditable { val

    createdBy: String? val createdAt: Instant? val updatedBy: String? val updatedAt: Instant? } object UserContext { const val DEFAULT_USERNAME = "system" val CURRENT_USER: ScopedValue<String?> = ScopedValue.newInstance() fun <T> withUser( username: String, block: () -> T, ): T = ScopedValue.where(CURRENT_USER, username).call(block) fun getCurrentUser(): String = runCatching { CURRENT_USER.get() }.getOrNull() ?: DEFAULT_USERNAME } abstract class AuditableIdTable<ID : Any>(name: String = "") : IdTable<ID>(name) { val createdBy = varchar("created_by", 50).clientDefault { UserContext.getCurrentUser() }.nullable() val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp).nullable() val updatedBy = varchar("updated_by", 50).nullable() val updatedAt = timestamp("updated_at").nullable() }
  41. Exposed DAO Auditable Table & Entity abstract class AuditableEntity<ID :

    Any>(id: EntityID<ID>): Entity<ID>(id), Auditable { override var createdBy: String? = null override var createdAt: Instant? = null override var updatedBy: String? = null override var updatedAt: Instant? = null // 엔티티가 업데이트될 때 실행되는 메서드 (flush 호출 전에 호출됨) override fun flush(batch: EntityBatchUpdate?): Boolean { if (writeValues.isNotEmpty() && createdAt != null) { // 업데이트 시간을 현재로 설정 updatedAt = Instant.now() updatedBy = UserContext.getCurrentUser() } if (createdAt == null) { // 생성 시간이 null 인 경우, 생성 시간과 생성자를 설정 createdAt = Instant.now() createdBy = UserContext.getCurrentUser() } return super.flush(batch) } }
  42. Exposed DAO Auditable Entity Example object TaskTable: AuditableIntIdTable("tasks") { val

    title = varchar("title", 200) val description = text("description") val status = varchar("status", 20).default("NEW") } class TaskEntity(id: EntityID<Int>): AuditableIntEntity(id) { companion object: EntityClass<Int, TaskEntity>(TaskTable) var title by TaskTable.title var description by TaskTable.description var status by TaskTable.status override var createdBy by TaskTable.createdBy override var createdAt by TaskTable.createdAt override var updatedBy by TaskTable.updatedBy override var updatedAt by TaskTable.updatedAt }
  43. Exposed DAO Auditable Entity Example object TaskTable: AuditableIntIdTable("tasks") { val

    title = varchar("title", 200) val description = text("description") val status = varchar("status", 20).default("NEW") } class TaskEntity(id: EntityID<Int>): AuditableIntEntity(id) { companion object: EntityClass<Int, TaskEntity>(TaskTable) var title by TaskTable.title var description by TaskTable.description var status by TaskTable.status override var createdBy by TaskTable.createdBy override var createdAt by TaskTable.createdAt override var updatedBy by TaskTable.updatedBy override var updatedAt by TaskTable.updatedAt } UserContext.withUser("test") { val now = java.time.Instant.now() Thread.sleep(100) // Task Create val task = TaskEntity.new { title = "Test Task" description = "This is a test task." status = "NEW" } entityCache.clear() // 생성 관련 정보만 있음 val loaded = TaskEntity.findById(task.id)!! loaded.createdAt.shouldNotBeNull() shouldBeGreaterOrEqualTo now loaded.createdBy.shouldNotBeNull() shouldBeEqualTo UserContext.getCurrentUser() loaded.updatedAt.shouldBeNull() loaded.updatedBy.shouldBeNull() // Task Update loaded.title = "Test Task - Updated" entityCache.clear() // 업데이트 관련 정보가 설정됨 val updated = TaskEntity.findById(task.id)!! updated.createdAt.shouldNotBeNull() shouldBeGreaterOrEqualTo now updated.createdBy.shouldNotBeNull() shouldBeEqualTo UserContext.getCurrentUser() updated.updatedAt.shouldNotBeNull() shouldBeGreaterOrEqualTo now updated.updatedBy.shouldNotBeNull() shouldBeEqualTo UserContext.getCurrentUser() }
  44. Exposed with R2DBC Truly IO Bounded Async/Non-Blocking Kotlin Exposed Statements

    RDBMS R2DBC Drivers https://github.com/bluetape4k/exposed-r2dbc-workshop
  45. Native SQL vs HQL vs CriteriaBuilder vs jOOQ ௪ܻߑध findAll()

    ࣗഋ DB findAll() ؀ഋ DB findAllByCategory() ઺ഋ DB Native SQL (JDBC) ӝળ (о੢ ࡅܴ) ӝળ ӝળ HQL (Hibernate) +120% וܿ +580ms וܿ 70% וܿ CriteriaBuilder HQL җ ਬࢎ HQL + 186ms וܿ 63% וܿ QueryDSL CriteriaBuilder ৬ ਬࢎ — — Exposed Native SQLী Ӕ੽ Native SQLী Ӕ੽ Native SQLী Ӕ੽
  46. Exposed JDBC vs JPA Single Entity (10 columns) CRUD ো࢑

    Exposed (μs) JPA (μs) Exposed ߓਯ create 1,196 ± 28 4,623 ± 100 3.9× ࡅܴ read (DAO find) 1,972 ± 54 11,602 ± 409 5.9× ࡅܴ update 1,980 ± 67 15,278 ± 596 7.7× ࡅܴ delete 1,945 ± 55 12,059 ± 398 6.2× ࡅܴ batchCreate (100Ѥ) 51,203 ± 640 423,816 ± 37,053 8.3× ࡅܴ readAll (ূ౭౭ ۽٘) 713 ± 13 705 ± 20 ਬࢎ (1.0×)
  47. Exposed JDBC vs JPA One-to-Many (Department : 20 Employees -

    8 + 12 Column) CRUD ো࢑ Exposed (μs) JPA (μs) Exposed ߓਯ create (1ࠗࢲ+20ݺ) 13,489 ± 675 87,556 ± 6,091 6.5× ࡅܴ read (DAO eager load) 16,493 ± 840 209,932 ± 15,693 12.7× ࡅܴ update (ࠗࢲ+੹૒ਗ) 14,448 ± 523 269,027 ± 5,002 18.6× ࡅܴ delete (CASCADE) 15,136 ± 1,024 226,083 ± 12,795 14.9× ࡅܴ batchCreate (10ࠗࢲƒ20ݺ) 136,876 ± 5,550 838,145 ± 59,213 6.1× ࡅܴ readAll (eager + JOIN FETCH) 716 ± 9 723 ± 20 ਬࢎ (1.0×)
  48. Exposed JDBC vs JPA ઙ೤ ࠺Ү Exposed ਋ਤ JPA ਋ਤ

    ◄────────────────────────────────────────┼──────────────────► Single Entity: create █████████ (3.9×) read ██████████████ (5.9×) update ███████████████████ (7.7×) delete ███████████████ (6.2×) batchCreate ████████████████████ (8.3×) readAll ▪ (ਬࢎ) One-to-Many: create ████████████████ (6.5×) read ████████████████████████████████ (12.7×) update ██████████████████████████████████████████████ (18.6×) delete █████████████████████████████████████████ (14.9×) batchCreate ███████████████ (6.1×) readAll ▪ (ਬࢎ) Concurrent (50 tasks): create ██████████ (2.8×) read █████████████ (4.2×) mixed ████████████ (4.0×)
  49. ࢿמ ର੉੄ Ӕࠄ ਗੋ JPA (Hibernate) য়ߡ೻٘ Exposed ੽Ӕ ߑध

    ೐۾द ё୓ ࢤࢿ + lazy loading DAO ૒੽ ݒೝ, eager loading ݺद dirty checking (೙٘ ߸҃ х૑) DSL۽ SQL ૒੽ ࢤࢿ 1ର நद ҙܻ ౟ے੥࣌ ߧਤ ղ ૒੽ ௪ܻ cascade ੹౵ + orphan removal ݺद੸ INSERT/DELETE JPQL → SQL ౵य ߸ജ Kotlin DSL → SQL ૒੽ ࠽٘
  50. ࢿמ ର੉੄ Ӕࠄ ਗੋ JPA (Hibernate) য়ߡ೻٘ Exposed ੽Ӕ ߑध

    ೐۾द ё୓ ࢤࢿ + lazy loading DAO ૒੽ ݒೝ, eager loading ݺद dirty checking (೙٘ ߸҃ х૑) DSL۽ SQL ૒੽ ࢤࢿ 1ର நद ҙܻ ౟ے੥࣌ ߧਤ ղ ૒੽ ௪ܻ cascade ੹౵ + orphan removal ݺद੸ INSERT/DELETE JPQL → SQL ౵य ߸ജ Kotlin DSL → SQL ૒੽ ࠽٘ ೐ۨ੐ਕ௼ ࢶఖ ӝળ
  51. ࢿמ ର੉੄ Ӕࠄ ਗੋ JPA (Hibernate) য়ߡ೻٘ Exposed ੽Ӕ ߑध

    ೐۾द ё୓ ࢤࢿ + lazy loading DAO ૒੽ ݒೝ, eager loading ݺद dirty checking (೙٘ ߸҃ х૑) DSL۽ SQL ૒੽ ࢤࢿ 1ର நद ҙܻ ౟ے੥࣌ ߧਤ ղ ૒੽ ௪ܻ cascade ੹౵ + orphan removal ݺद੸ INSERT/DELETE JPQL → SQL ౵य ߸ജ Kotlin DSL → SQL ૒੽ ࠽٘ ೐ۨ੐ਕ௼ ࢶఖ ӝળ • ࢿמ ୭਋ࢶ, SQL ઁয ೙ਃ → Exposed (੹ ৔৉ 3~19ߓ ࡅܴ)
  52. ࢿמ ର੉੄ Ӕࠄ ਗੋ JPA (Hibernate) য়ߡ೻٘ Exposed ੽Ӕ ߑध

    ೐۾द ё୓ ࢤࢿ + lazy loading DAO ૒੽ ݒೝ, eager loading ݺद dirty checking (೙٘ ߸҃ х૑) DSL۽ SQL ૒੽ ࢤࢿ 1ର நद ҙܻ ౟ے੥࣌ ߧਤ ղ ૒੽ ௪ܻ cascade ੹౵ + orphan removal ݺद੸ INSERT/DELETE JPQL → SQL ౵य ߸ജ Kotlin DSL → SQL ૒੽ ࠽٘ ೐ۨ੐ਕ௼ ࢶఖ ӝળ • ࢿמ ୭਋ࢶ, SQL ઁয ೙ਃ → Exposed (੹ ৔৉ 3~19ߓ ࡅܴ) • ࠂ੟ೠ بݫੋ ݽ؛ + 2ର நद + Spring Data ࢤక҅ → JPA with Virtual Threads
  53. ࢿמ ର੉੄ Ӕࠄ ਗੋ JPA (Hibernate) য়ߡ೻٘ Exposed ੽Ӕ ߑध

    ೐۾द ё୓ ࢤࢿ + lazy loading DAO ૒੽ ݒೝ, eager loading ݺद dirty checking (೙٘ ߸҃ х૑) DSL۽ SQL ૒੽ ࢤࢿ 1ର நद ҙܻ ౟ے੥࣌ ߧਤ ղ ૒੽ ௪ܻ cascade ੹౵ + orphan removal ݺद੸ INSERT/DELETE JPQL → SQL ౵य ߸ജ Kotlin DSL → SQL ૒੽ ࠽٘ ೐ۨ੐ਕ௼ ࢶఖ ӝળ • ࢿמ ୭਋ࢶ, SQL ઁয ೙ਃ → Exposed (੹ ৔৉ 3~19ߓ ࡅܴ) • ࠂ੟ೠ بݫੋ ݽ؛ + 2ର நद + Spring Data ࢤక҅ → JPA with Virtual Threads • ֫਷ زदࢿ → Exposed ӂ੢ (Virtual Threadsח ਗѺ DB ജ҃ীࢲ ୶о ੉੼)
  54. ࢶఖ о੉٘ ࢶఖ ੸೤ೠ ҃਋ Exposed Kotlin-first, SQL ઁযӂ ೙ਃ,

    ծ਷ য়ߡ೻٘, N+1 ߑ૑, Coroutines ࢎਊ, R2DBC ੹ജ, Near Cache ഝਊ Hibernate 2ର நद ഝਊ, ࠂ੟ೠ بݫੋ ݽ؛, Spring Data JPA ࢤక҅ Plain JDBC Ҋࢿמ, рױೠ ௪ܻ, ೐ۨ੐ਕ௼ য়ߡ೻٘ ઁѢ R2DBC, Exposed R2DBC ୭؀ ࢿמ, ؀ਊ۝ ୊ܻ, ୭ࣗ Latency
  55. ࢶఖ о੉٘ ࢶఖ ੸೤ೠ ҃਋ Exposed Kotlin-first, SQL ઁযӂ ೙ਃ,

    ծ਷ য়ߡ೻٘, N+1 ߑ૑, Coroutines ࢎਊ, R2DBC ੹ജ, Near Cache ഝਊ Hibernate 2ର நद ഝਊ, ࠂ੟ೠ بݫੋ ݽ؛, Spring Data JPA ࢤక҅ Plain JDBC Ҋࢿמ, рױೠ ௪ܻ, ೐ۨ੐ਕ௼ য়ߡ೻٘ ઁѢ R2DBC, Exposed R2DBC ୭؀ ࢿמ, ؀ਊ۝ ୊ܻ, ୭ࣗ Latency • զݡ਷?
  56. ࢶఖ о੉٘ ࢶఖ ੸೤ೠ ҃਋ Exposed Kotlin-first, SQL ઁযӂ ೙ਃ,

    ծ਷ য়ߡ೻٘, N+1 ߑ૑, Coroutines ࢎਊ, R2DBC ੹ജ, Near Cache ഝਊ Hibernate 2ର நद ഝਊ, ࠂ੟ೠ بݫੋ ݽ؛, Spring Data JPA ࢤక҅ Plain JDBC Ҋࢿמ, рױೠ ௪ܻ, ೐ۨ੐ਕ௼ য়ߡ೻٘ ઁѢ R2DBC, Exposed R2DBC ୭؀ ࢿמ, ؀ਊ۝ ୊ܻ, ୭ࣗ Latency • զݡ਷? • Java 21/25 + Virtual Threads + JPA
  57. ࢶఖ о੉٘ ࢶఖ ੸೤ೠ ҃਋ Exposed Kotlin-first, SQL ઁযӂ ೙ਃ,

    ծ਷ য়ߡ೻٘, N+1 ߑ૑, Coroutines ࢎਊ, R2DBC ੹ജ, Near Cache ഝਊ Hibernate 2ର நद ഝਊ, ࠂ੟ೠ بݫੋ ݽ؛, Spring Data JPA ࢤక҅ Plain JDBC Ҋࢿמ, рױೠ ௪ܻ, ೐ۨ੐ਕ௼ য়ߡ೻٘ ઁѢ R2DBC, Exposed R2DBC ୭؀ ࢿמ, ؀ਊ۝ ୊ܻ, ୭ࣗ Latency • զݡ਷? • Java 21/25 + Virtual Threads + JPA • Java 21/25 + Virtual Threads + Exposed JDBC
  58. ࢶఖ о੉٘ ࢶఖ ੸೤ೠ ҃਋ Exposed Kotlin-first, SQL ઁযӂ ೙ਃ,

    ծ਷ য়ߡ೻٘, N+1 ߑ૑, Coroutines ࢎਊ, R2DBC ੹ജ, Near Cache ഝਊ Hibernate 2ର நद ഝਊ, ࠂ੟ೠ بݫੋ ݽ؛, Spring Data JPA ࢤక҅ Plain JDBC Ҋࢿמ, рױೠ ௪ܻ, ೐ۨ੐ਕ௼ য়ߡ೻٘ ઁѢ R2DBC, Exposed R2DBC ୭؀ ࢿמ, ؀ਊ۝ ୊ܻ, ୭ࣗ Latency • զݡ਷? • Java 21/25 + Virtual Threads + JPA • Java 21/25 + Virtual Threads + Exposed JDBC • Java 17+ + Coroutines + Exposed R2DBC
  59. Resources • Spring Data Relational / R2DBC • Hibernate Reactive

    • Vert.x SQL Client Templates, MyBatis Dynamic SQL • Kotlin Exposed Documentation • Bluetape4k Exposed Workshop, Exposed R2DBC Workshop • Kotlin Exposed Book by Debop