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

Alternatives to JPA (Async/Non-Blocking) 2025

Alternatives to JPA (Async/Non-Blocking) 2025

This document presents an approach for replacing JPA to enable asynchronous and non-blocking access to relational databases, optimized for large-scale data processing.

Avatar for Sunghyouk Bae

Sunghyouk Bae

May 09, 2025
Tweet

More Decks by Sunghyouk Bae

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 ী ૘઺

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

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

    ػ ٜ࠙਷ ઁҕػ ӝמ݅ ࢎਊ • spring-data-jpa ӝળਵ۽݅ ࢎਊ • ূ౭౭ী hashCode, equals, toString ੤੿੄о হ਺ • Relation ࢸ੿੉ ׮নࢿ੉ হ਺ • Poor performance -> “JPA is bad” ۄҊ য়ੋೡ ৈ૑о ݆਺ • ੸೤ೞ૑ ঋח ா੉झী ࢎਊ (ా҅, Bulk ੘স) • ؀উ੉ ੓਺ীب ࢎਊೞ૑ ঋ਺ • Stateless Session, 2nd cache …
  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 ܳ ࢎਊೞѢա `Stateless Session` ਸ ࢎਊೞחо?
  7. ജ҃ ߸ച Async/Non-Blocking • High throughput ೙ਃࢿ੉ ֫ই૗ • Async/Non-Blocking

    ജ҃੉ ೙ਃ (JDBC ח زӝߑध) • NoSQL ਸ ઁ؀۽ ഝਊೡ ࣻ ੓ח case ח ઁೠؽ (਍৔ ҃೷) • ৈ۞о૑ ؀উٜ • IO-Bounded • R2DBC, Exposed with R2DBC (Under development) • Hibernate-Reactive, Vert.x Sql Client, • CPU-Bounded • Exposed (Coroutines), 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 val matcher = Person::class .buildExampleMatcher(Person::firstname.name, Person::lastname.name) .withMatcher(Person::firstname.name, GenericPropertyMatchers.startsWith()) .withMatcher(Person::lastname.name,

    GenericPropertyMatchers.ignoreCase()) .withIgnoreNullValues() val example = Example.of(Person("Walter", "WHITE", 0), matcher) repository.findAll(example).asFlow().toList() shouldContainSame listOf(walter, flynn) interface PersonRepository: ReactiveCrudRepository<Person, Int>, ReactiveQueryByExampleExecutor<Person> data class Person( val firstname: String, val lastname: String, val age: Int, ): Serializable { @Id var id: Int? = null val hasId: Boolean get() = id != null }
  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 @Repository class PostRepository( private val client: DatabaseClient, private val operations: R2dbcEntityOperations, private val mappingR2dbcConverter: MappingR2dbcConverter, ) { suspend fun count(): Long = operations.coCountAll<Post>() fun findAll(): Flow<Post> = operations.coSelectAll() suspend fun findOneById(id: Long): Post = operations.coFindOneById(id) suspend fun findOneByIdOrNull(id: Long): Post? = operations.coFindOneByIdOrNull(id) suspend fun findFirstById(id: Long): Post = operations.coFindFirstById(id) suspend fun findFirstByIdOrNull(id: Long): Post? = operations.coFindFirstByIdOrNull(id) suspend fun deleteAll(): Long = operations.coDeleteAll<Post>() suspend fun save(post: Post): Post = operations.coInsert(post) suspend fun init() { save(Post(title = "My first post title", content = "Content of my first post")) save(Post(title = "My second post title", content = "Content of my second post")) } } @Table("comments") data class Comment( @Column("content") val content: String? = null, @Column("post_id") val postId: Long? = null, @Id val id: Long? = null, ): Serializable
  12. R2DBC ೠ҅ • ࠂ੟ೠ ௪ܻח String ਵ۽ ੘স೧ঠ ೣ •

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

    • ূ౭౭ ੿੄ח ӝઓ JPA/Hibernate ৬ э਺ • Async/Non-Blocking ҳഅ ߑध • Reactive ߑध - Mutiny ࢎਊ (೟ण ೙ਃ) • CompletionStage ߑध • Vert.x Sql Client ܳ Database Driver ۽ ࢎਊ • Quarkusо ҕध ૑ਗ (Spring ޷૑ਗ) • Using Hibernate Reactive, Hibernate Reactive with Panache
  14. One To Many Relation Author - Books Entity @Table(name =

    "authors") @Access(AccessType.FIELD) class Author private constructor( @Column(nullable = false) val name: String, ): AbstractValueObject() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L @OneToMany( mappedBy = "author", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY ) var books: MutableList<Book> = mutableListOf() } // NOTE: author ܳ lazy ۽ ঳ӝ ਤ೧ࢲח @FetchProfile ਸ ੉ਊ೧ঠ ೤פ׮. @FetchProfile( name = "withAuthor", fetchOverrides = [ FetchProfile.FetchOverride( entity = Book::class, association = "author", mode = FetchMode.JOIN ) // അ੤ח FetchMode.JOIN ݅ ૑ਗೠ׮ ] ) @Entity @Table(name = "books") class Book private constructor( val isbn: String, val title: String, @Past var published: LocalDate, ): AbstractValueObject() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0L // NOTE: ManyToOne ੄ FetchTypeਸ LAZY ۽ ೞݶ Thread ߧਤܳ ߩযա ৘৻о ߊࢤೠ׮ // NOTE: ੉ۡ ٺ LEFT JOIN FETCH ܳ ࣻ೯ೞ؍о @FetchProfile ਸ ࢎਊ೧ঠ ೠ׮ @ManyToOne(optional = false, fetch = FetchType.LAZY) var author: Author? = null }
  15. Query by HQL Book - Author @Test fun `find all

    book with fetch join`() = runSuspendIO { val sql = "SELECT b FROM Book b LEFT JOIN FETCH b.author a" val books = sf.withSessionSuspending { session -> session.createSelectionQuery(sql, Book::class.java).resultList.awaitSuspending() } books.forEach { println("book=$it, author=${it.author}") } books shouldHaveSize 3 } select b1_0.id, b1_0.author_id, a1_0.id, a1_0.name, b1_0.isbn, b1_0.published, b1_0.title from books b1_0 left join authors a1_0 on a1_0.id=b1_0.author_id
  16. Query by JPQL & Entity Graph Mutiny with Coroutines @Test

    fun `find all by entity graph`() = runSuspendIO { val criteria = sf.criteriaBuilder.createQuery(Book::class.java) val root = criteria.from(Book::class.java) criteria.select(root) val books = sf.withSessionSuspending { session -> val graph = session.createEntityGraph(Book::class.java) graph.addAttributeNodes(Book::author.name) val query: Mutiny.SelectionQuery<Book> = session.createQuery(criteria) query.setPlan(graph) query.resultList.awaitSuspending() } books.forEach { println("book=$it, author=${it.author}”) } books shouldHaveSize 3 } select b1_0.id, b1_0.author_id, a1_0.id, a1_0.name, b1_0.isbn, b1_0.published, b1_0.title from books b1_0 join authors a1_0 on a1_0.id=b1_0.author_id
  17. Vert.x Sql Client • True IO-Bounded Async/Non-Blocking Library • Use

    Netty • Hibernate-Reactive use vertx-sql-client • SqlTemplate • Cons • Support Raw SQL Statement only, no Typesafe DSL • JSON ١ ౠࣻ ࣻഋী ؀ೠ ୊ܻо ࠛউ੿ೣ
  18. Using SqlTemplate val pool = vertx.getH2Pool() vertx.testWithTransactionSuspending(testContext, pool) { val

    parameters = mapOf( "id" to 3, "firstName" to "Dale", "lastName" to "Cooper" ) val result: SqlResult<Void> = SqlTemplate .forUpdate(pool, "INSERT INTO users VALUES(#{id}, #{firstName}, #{lastName})") .execute(parameters) .coAwait() result.rowCount() shouldBeEqualTo 1 } pool.close().coAwait() val pool = vertx.getH2Pool() vertx.testWithTransactionSuspending(testContext, pool) { val parameters = mapOf("id" to 1) val users: RowSet<User> = SqlTemplate .forQuery(pool, "SELECT * FROM users WHERE id = #{id}") .mapTo(USER_ROW_MAPPER) .execute(parameters) .coAwait() users.size() shouldBeEqualTo 1 users.forEach { user -> log.debug { user } } } pool.close().coAwait()
  19. 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
  20. 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") }
  21. Execute by Vert.x Sql Client 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
  22. 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 (CPU-Bounded Async/Non-Blocking)
  23. Exposed - SQL DSL De fi ne Table & Relations

    object ActorsInMovies: Table("actors_in_movies") { val actorId = integer("actor_id").references(Actors.id, onDelete = ReferenceOption.CASCADE) val movieId = integer("movie_id").references(Movies.id, onDelete = ReferenceOption.CASCADE) override val primaryKey = PrimaryKey(movieId, actorId) } object Movies: IntIdTable("movies") { val name = varchar("name", 255) val producerName = varchar("producer_name", 255) val releaseDate = datetime("release_date") } object Actors: IntIdTable("actors") { val firstName = varchar("first_name", 255) val lastName = varchar("last_name", 255) val dateOfBirth = date("date_of_birth").nullable() }
  24. Exposed SQL DSL - batchInsert val actorDTOs = Actors.batchInsert(actors) {

    this[Actors.firstName] = it.firstName this[Actors.lastName] = it.lastName it.dateOfBirth?.let { birthDay -> this[Actors.dateOfBirth] = LocalDate.parse(birthDay) } }.map { it.toActorDTO() } val movieDTOs = Movies.batchInsert(movies) { this[Movies.name] = it.name this[Movies.producerName] = it.producerName this[Movies.releaseDate] = LocalDate.parse(it.releaseDate).atTime(0, 0) }.map { it.toMovieDTO() } ActorsInMovies.batchInsert(movieActorIds) { this[ActorsInMovies.movieId] = it.first this[ActorsInMovies.actorId] = it.second }
  25. Exposed SQL DSL Query by Join - One-to-Many suspend fun

    getAllMoviesWithActors(): List<MovieWithActorDTO> { return newSuspendedTransaction { MovieInnerJoinActors .selectAll() .groupingBy { it[Movies.id] } .fold(mutableListOf<MovieWithActorDTO>()) { acc, element -> val lastMovieId = acc.lastOrNull()?.id if (lastMovieId != element[Movies.id].value) { val movie = MovieWithActorDTO( id = element[Movies.id].value, name = element[Movies.name], producerName = element[Movies.producerName], releaseDate = element[Movies.releaseDate].toString(), ) acc.add(movie) } else { acc.lastOrNull()?.actors?.let { val actor = ActorDTO( id = element[Actors.id].value, firstName = element[Actors.firstName], lastName = element[Actors.lastName], dateOfBirth = element[Actors.dateOfBirth].toString() ) it.add(actor) } } acc } .values .flatten() } } private val MovieInnerJoinActors by lazy { Movies .innerJoin(ActorsInMovies) .innerJoin(Actors) }
  26. Exposed SQL DSL With Coroutines suspend fun searchMovie(params: Map<String, String>):

    List<MovieDTO> = newSuspendedTransaction { log.debug { "Search Movie by params. params: $params" } val query = Movies.selectAll() params.forEach { (key, value) -> when (key) { "id" -> query.andWhere { Movies.id eq value.toInt() } "name" -> query.andWhere { Movies.name eq value } "producerName" -> query.andWhere { Movies.producerName eq value } "releaseDate" -> query.andWhere { Movies.releaseDate eq LocalDateTime.parse(value) } } } query.map { it.toMovieDTO() } } suspend fun create(movie: MovieDTO): MovieDTO? = newSuspendedTransaction { val movieId = Movies.insertAndGetId { it[Movies.name] = movie.name it[Movies.producerName] = movie.producerName if (movie.releaseDate.isNotBlank()) { it[Movies.releaseDate] = LocalDate.parse(movie.releaseDate).atTime(0, 0) } } movie.copy(id = movieId.value) }
  27. Exposed SQL DSL In Virtual Threads @Test fun `get all

    actors in multiple virtual threads`() { VirtualthreadTester() .numThreads(Runtimex.availableProcessors * 2) .roundsPerThread(4) .add { transaction(db) { val actors = Actors.selectAll().map { it.toActorDTO() } actors.shouldNotBeEmpty() } } .run() } fun `get all actors in virtual threads`() { virtualFuture { transaction { val actors = Actors.selectAll().map { it.toActorDTO() } actors.shouldNotBeEmpty() } }.await() } 2025-02-09 00:27:53.688 DEBUG 78219 --- [ bluetape4k-test-vt-2] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.DATE_OF_BIRTH FROM ACTORS 2025-02-09 00:27:53.688 DEBUG 78219 --- [ bluetape4k-test-vt-1] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.DATE_OF_BIRTH FROM ACTORS 2025-02-09 00:27:53.688 DEBUG 78219 --- [ bluetape4k-test-vt-5] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.DATE_OF_BIRTH FROM ACTORS 2025-02-09 00:27:53.689 DEBUG 78219 --- [ bluetape4k-test-vt-3] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.DATE_OF_BIRTH FROM ACTORS 2025-02-09 00:27:53.689 DEBUG 78219 --- [ bluetape4k-test-vt-0] Exposed : SELECT ACTORS.ID, ACTORS.FIRST_NAME, ACTORS.LAST_NAME, ACTORS.DATE_OF_BIRTH FROM ACTORS
  28. Exposed - DAO Lightweight ORM class Movie(id: EntityID<Int>): IntEntity(id), Serializable

    { companion object: IntEntityClass<Movie>(Movies) var name by Movies.name var producerName by Movies.producerName var releaseDate by Movies.releaseDate override fun equals(other: Any?): Boolean = idEquals(other) override fun hashCode(): Int = idHashCode() override fun toString(): String = toStringBuilder() .add("name", name) .add("producerName", producerName) .add("releaseDate", releaseDate) .toString() } class Actor(id: EntityID<Int>): IntEntity(id), Serializable { companion object: IntEntityClass<Actor>(Actors) var firstName by Actors.firstName var lastName by Actors.lastName var dateOfBirth by Actors.dateOfBirth override fun equals(other: Any?): Boolean = idEquals(other) override fun hashCode(): Int = idHashCode() override fun toString(): String = toStringBuilder() .add("firstName", firstName) .add("lastName", lastName) .add("dateOfBirth", dateOfBirth) .toString() }
  29. Exposed DAO With Coroutines @Transactional(readOnly = true) suspend fun findById(id:

    Int): ActorDTO? { log.debug { "Find Actor by id. id: $id" } return newSuspendedTransaction { Actor.findById(id)?.toActorDTO() } } @Transactional suspend fun createByDAO(actor: ActorDTO): ActorDTO { log.debug { "Create Actor. actor: $actor" } return newSuspendedTransaction { val newActor = Actor.new { firstName = actor.firstName lastName = actor.lastName dateOfBirth = actor.dateOfBirth?.let { LocalDate.parse(it) } } newActor.toActorDTO() } }
  30. Exposed DAO Insert & Find in Virtual Threads fun create(actor:

    ActorDTO): VirtualFuture<ActorDTO> = virtualFuture(virtualExecutor) { log.debug { "Create Actor. actor: $actor" } transaction(db) { val actor = Actor.new { firstName = actor.firstName lastName = actor.lastName actor.dateOfBirth?.let { dateOfBirth = LocalDate.parse(it) } } actor.toActorDTO() } } fun findById(id: Int): VirtualFuture<ActorDTO?> = virtualFuture(virtualExecutor) { log.debug { "Find Actor by id. id: $id" } transaction(db) { Actor.findById(id)?.toActorDTO() } }
  31. ౠ੿ ۄ੉࠳۞ܻ ࢶఖ о੉٘ ۄੋ • Ҋࢿמ, ؀ਊ۝ -> Exposed

    with R2DBC (Exposed 1.0.0 ী ನೣ ৘੿) • рױೠ ҙ҅, ױࣽ ূ౭౭ ਤ઱ -> R2DBC, Hibernate-Reactive, Exposed DAO • ؀۝੄ ؘ੉ఠ ੑ୹۱ -> R2DBC, Vert.x Sql Client, Exposed DSL • ࠂ੟ೠ ҙ҅, ׮নೠ ૘҅ -> Vert.x Sql Client, Exposed DSL • Kotlin Coroutines ജ҃ -> R2DBC, Vert.x Sql Client, Exposed • ࠂ੟ೠ ࣻഋ (JSON, Array, Encryption …) -> Hibernate-Reactive, Exposed • Spring Boot Integration - R2DBC, Exposed • զݡ਷? -> Java 21 Virtual Threads + JPA | Exposed
  32. Resources • Spring Data Relational / R2DBC • Hibernate Reactive

    • Vert.x SQL Client Templates • MyBatis Dynamic SQL • Kotlin Exposed Documentation • Bluetape4k Workshop • Kotlin Exposed Book