Share
Embed
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.
Sunghyouk Bae (Debop)
May 09, 2025
Transcript
44 slides
-
Debop - Retired Backend Developer, 2025-04-16 Alternatives to JPA 2025
Async/Non-Blocking Library -
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 -
ORM (Object Relational Mapping) • Object Graph vs Relational Data
• Business Logic ਸ OOP ۽ അ, Not Relational Data • Hibernate ݾ : ాੋ SQL ޙ 95%ܳ • DB Vendors ী ޖҙೠ ٘ ࢤࢿ оמ (Solution স҅) • ORM Ҋࢿמ, data centric ࠙ঠীח ೞ ঋ -
JPA • Java Object Graph & OOP ী
• दझమ ࢸ҅ী ೡ ࣻ • Entity ٜ ҙ҅ա ઁড ઑѤী ೧ ࣘࢿਵ۽ അ • ౠ DB Vendor ౠࢿਸ ঌ ਃ হ • SQL ҳޙਸ ߓ ਃ হ (?) • HQL, JPQL, QueryDSL ਸ न ߓਕঠ ೣ • Stateful (default), Stateless Session ਗ -
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 ١ -
JPA ࢎਊ അട • JPA о ܨо ػ दী ѐߊо
ػ ٜ࠙ ઁҕػ ӝמ݅ ࢎਊ • spring-data-jpa ӝળਵ۽݅ ࢎਊ • ূ౭౭ী hashCode, equals, toString о হ • Relation ࢸ নࢿ হ • Poor performance -> “JPA is bad” ۄҊ য়ੋೡ ৈо ݆ • ೞ ঋח ாझী ࢎਊ (ా҅, Bulk স) • উ ীب ࢎਊೞ ঋ • Stateless Session, 2nd cache … -
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` ਸ ࢎਊೞחо? -
ജ҃ ߸ച 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) -
R2DBC ਃ ౠ • Async/Non-Blocking • Reactive Stream • Low
resources • Back pressure ܳ ాೠ ؘఠ ܴਸ ઁয • ਗೞח Driver • Oracle, SQL Server, MariaDB, MySQL, Postgres, H2 -
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 -
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 } -
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 -
R2DBC ೠ҅ • ࠂೠ ௪ܻח String ਵ۽ স೧ঠ ೣ •
MyBatis Dynamic SQL ١ DSL بਵ۽ оמ (ప࠶ ਃ) • নೠ Relation ী ೠ ࠽٘ܳ ࣻসਵ۽ ࣻ೯೧ঠ ೠ. -
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 -
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 } -
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 -
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 -
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 ١ ౠࣻ ࣻഋী ೠ ܻо ࠛউೣ -
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() -
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 -
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") } -
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 -
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) -
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() } -
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 } -
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) } -
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) } -
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 -
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() } -
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() } } -
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() } } -
Exposed with R2DBC Truly IO Bounded Async/Non-Blocking Kotlin Exposed Statements
RDBMS R2DBC Under development (Exposed 1.0.0) -
ౠ ۄ࠳۞ܻ ࢶఖ о٘ ۄੋ • Ҋࢿמ, ਊ -> 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 -
Resources • Spring Data Relational / R2DBC • Hibernate Reactive
• Vert.x SQL Client Templates • MyBatis Dynamic SQL • Kotlin Exposed Documentation • Bluetape4k Workshop • Kotlin Exposed Book