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) }
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) } }
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() } }
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
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") }
• 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)
companion object: IntEntityClass<BankAccount>(BankAccountTable) var number: String by BankAccountTable.number // many to many with via val owners: SizedIterable<AccountOwner> by AccountOwner via OwnerAccountMapTable }
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 }
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) } }
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 }
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() }