Kotlin Backend Jpa Entity Mapping

kotlin/kotlin-agent-skills/skills/kotlin-backend-jpa-entity-mapping

作者 kotlinc2f90697bf71Apache-2.01K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫今天更新

Model Kotlin persistence code correctly for Spring Data JPA and Hibernate. Covers entity design, identity and equality, uniqueness constraints, relationships, fetch plans, and common ORM (Object-Relational Mapping) traps specific to Kotlin. Use when creating or reviewing JPA (Java Persistence API) entities, diagnosing N+1 or LazyInitializationException, placing indexes and uniqueness rules, or preventing Kotlin-specific bugs such as data class entities and broken equals/hashCode.

AI 產生的概覽

指導 Kotlin JPA/Hibernate 實體的正確對應、識別、唯一性與抓取計畫。

功能
此技能為使用 Spring Data JPA 與 Hibernate 的 Kotlin 持久化程式碼建模提供指引。內容涵蓋實體設計規則、識別與相等性策略、唯一性限制、查詢與抓取計畫,以及 Kotlin 特有的常見 ORM 陷阱。它產出的是建議與程式碼模式(含正誤範例),而非產生檔案或指令碼。
適用情境
適用於建立或審查 Kotlin 中的 JPA 實體、診斷 N+1 查詢或 LazyInitializationException、設定索引與唯一性規則,或避免 data class 實體、equals/hashCode 失效等 Kotlin 特有缺陷。
執行需求
不包含指令碼,僅為說明性指示。假定使用 Spring Data JPA 與 Hibernate 的 Kotlin 專案,並了解 kotlin("plugin.jpa") 無參數外掛與 JPA 註解。

JPA Entity Mapping for Kotlin

Kotlin's data class is natural for DTOs but dangerous for JPA entities. Hibernate relies on identity semantics that data class breaks: equals/hashCode over all fields corrupts Set/Map membership after state changes, and auto-generated copy() creates detached duplicates of managed entities.

This skill teaches correct entity design, identity strategies, and uniqueness constraints for Kotlin + Spring Data JPA projects.

Entity Design Rules

  • Never use data class for JPA entities. Use a regular class. Keep data class for DTOs.
  • Keep transport DTOs and persistence entities separate unless the project clearly uses a shared model.
  • Model required columns as non-null only when object construction and persistence lifecycle make it safe.
  • Use lateinit only when the project already accepts that tradeoff and the lifecycle is safe.
  • Verify kotlin("plugin.jpa") or equivalent no-arg support when JPA entities exist.
  • Verify classes and members are compatible with proxying where needed.

Identity and Equality

  • Never accept all-field equals/hashCode generated by data class on an entity.
  • Follow project conventions when they already define an identity strategy.
  • If no convention exists, use ID-based equality with a stable hashCode.
  • For DB-generated IDs, model the unsaved state with nullable var id: Long? = null and a protected set; do not use 0L as a sentinel value.
  • Be explicit about mutable fields and lazy associations when discussing equality.

Broken: data class Entity

kotlin
// WRONG: data class generates equals/hashCode from ALL fields,// and the generated ID uses a 0 sentinel instead of nulldata class Order(    @Id @GeneratedValue val id: Long = 0,    var status: String,    var total: BigDecimal)// BUG: order.status = "SHIPPED"; set.contains(order) → false (hash changed)// BUG: Hibernate proxy.equals(entity) → false (proxy has lazy fields uninitialized)

Correct: Regular Class with ID-Based Identity

kotlin
@Entity@Table(name = "orders")class Order(    @Column(nullable = false)    var status: String,
    @Column(nullable = false)    var total: BigDecimal) {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    var id: Long? = null        protected set
    override fun equals(other: Any?): Boolean {        if (this === other) return true        if (other !is Order) return false        return id != null && id == other.id    }
    override fun hashCode(): Int = javaClass.hashCode()
    // toString must NOT reference lazy collections    override fun toString(): String = "Order(id=$id, status=$status)"}

Key rules:

  • equals compares by ID only — stable under dirty tracking and proxy unwrapping
  • hashCode returns class-based constant — avoids Set/Map corruption after persist
  • toString excludes lazy-loaded relations — prevents LazyInitializationException
  • Constructor params are mutable entity fields; DB-generated id is nullable with a protected setter

Uniqueness Constraints

When an API must be idempotent (e.g., "reserve stock for order X"), enforce uniqueness at both layers: database constraint for correctness, application check for clean errors.

Broken: No Duplicate Guard

kotlin
@Serviceclass ReservationService(private val repo: ReservationRepository) {    @Transactional    fun createReservation(variantId: Long, orderId: String, qty: Int): Reservation {        // BUG: no check — duplicates silently accumulate        return repo.save(Reservation(variantId = variantId, orderId = orderId, quantity = qty))    }}

Correct: Database Constraint + Application Guard

kotlin
@Entity@Table(    name = "reservations",    uniqueConstraints = [        UniqueConstraint(columnNames = ["variant_id", "order_id"])    ])class Reservation(    @Column(name = "variant_id", nullable = false)    val variantId: Long,
    @Column(name = "order_id", nullable = false)    val orderId: String,
    @Column(nullable = false)    var quantity: Int) {    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)    var id: Long? = null        protected set}
interface ReservationRepository : JpaRepository<Reservation, Long> {    fun findByVariantIdAndOrderId(variantId: Long, orderId: String): Reservation?}
@Serviceclass ReservationService(private val repo: ReservationRepository) {    @Transactional    fun createReservation(variantId: Long, orderId: String, qty: Int): Reservation {        repo.findByVariantIdAndOrderId(variantId, orderId)?.let {            throw IllegalStateException(                "Reservation already exists for variant=$variantId, order=$orderId"            )        }        return repo.save(Reservation(variantId = variantId, orderId = orderId, quantity = qty))    }}

Key rules:

  • Database constraint is mandatory — application checks alone have race conditions
  • Application check provides clean error messages — without it, users get raw DataIntegrityViolationException
  • Both layers together: application catches the common case, database catches the race
  • Spring Data derives findByXAndY queries automatically

Query and Fetch Rules

  • Diagnose N+1 by looking at actual query count or SQL logs, not by guessing from annotations.
  • Prefer targeted fetch solutions: @EntityGraph, JOIN FETCH, batch fetching, or DTO projection.
  • Be careful with collection fetch joins plus pagination — call out the tradeoff.
  • Use indexes and uniqueness constraints to support real query patterns.

Common ORM Traps

  • Bidirectional associations: maintain both sides in domain methods. Half-updated graphs cause subtle bugs.
  • orphanRemoval vs cascade remove: not interchangeable. Explain lifecycle semantics before choosing.
  • Lazy load triggers: toString, debug logging, JSON serialization, and IDE inspection can all trigger lazy loads.
  • Bulk updates/deletes: bypass persistence context and lifecycle callbacks. Subsequent reads may be stale.
  • Multiple bag fetches: can cause Cartesian explosion. Verify the ORM can execute collection-heavy fetch plans safely.
  • Set + mutable equality: collection membership can break after entity state changes.
  • @Version: the clearest optimistic concurrency mechanism when concurrent updates matter.
  • open-in-view disabled: DTO mapping touching lazy fields must happen inside a transaction boundary.

Guardrails

  • Do not use data class for JPA entities.
  • Do not recommend FetchType.EAGER everywhere to silence lazy loading symptoms.
  • Do not expose entities directly through API responses by default.
  • Do not claim an N+1 fix without explaining how the fetch plan changes query behavior.

來源與署名

來源:kotlin/kotlin-agent-skills位於skills/kotlin-backend-jpa-entity-mapping提交c2f9069

授權條款: Apache-2.0

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架