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 (Debop)

Sunghyouk Bae (Debop)

May 09, 2025
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 ী ૘઺

    • दझమ ࢸ҅ী ૘઺ೡ ࣻ ੓਺ • 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