PluginBench
Skill
Official
Pass
Audit score 90

kotlin-backend-jpa-entity-mapping

kotlin/kotlin-agent-skills

Model Kotlin JPA entities correctly—avoid data class pitfalls, identity bugs, and lazy-loading traps.

What is kotlin-backend-jpa-entity-mapping?

Teaches correct JPA entity design for Kotlin + Spring Data projects. Covers identity and equality semantics, uniqueness constraints, fetch strategies, and Kotlin-specific ORM traps like data class entities breaking equals/hashCode and triggering LazyInitializationException.

  • Design JPA entities as regular classes, not data classes, with stable ID-based equality
  • Implement correct equals/hashCode and toString to avoid Set/Map corruption and lazy-load exceptions
  • Apply database uniqueness constraints paired with application-level guards for idempotent operations
  • Diagnose and fix N+1 queries using @EntityGraph, JOIN FETCH, and batch fetching
  • Avoid bidirectional association bugs, orphanRemoval vs cascade tradeoffs, and bulk-update staleness

How to install kotlin-backend-jpa-entity-mapping

npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping
Prerequisites
  • Spring Data JPA or Hibernate on the classpath
  • Kotlin JPA plugin (kotlin("plugin.jpa")) configured in build.gradle.kts
  • Basic familiarity with JPA annotations and Spring Data repositories
Claude Code
Cursor
Windsurf
Cline

How to use kotlin-backend-jpa-entity-mapping

  1. 1.Replace any data class entities with regular classes and implement ID-based equals/hashCode
  2. 2.Verify entity constructors accept mutable fields and DB-generated IDs are nullable with protected setters
  3. 3.Add @UniqueConstraint annotations to @Table for business-critical uniqueness rules
  4. 4.Implement application-level guards (repository queries) before save() to catch duplicates early
  5. 5.Use @EntityGraph or JOIN FETCH to eliminate N+1 queries; verify with SQL logs or query counts
  6. 6.Exclude lazy-loaded collections from toString() and equals() to prevent LazyInitializationException

Use cases

Good for
  • Creating a new JPA entity model and choosing between data class vs regular class
  • Debugging LazyInitializationException or Set membership failures in existing entities
  • Adding uniqueness constraints to prevent duplicate reservations or orders
  • Diagnosing N+1 query problems and selecting the right fetch strategy
  • Reviewing entity design to ensure Hibernate proxying and dirty tracking work correctly
Who it's for
  • Kotlin backend developers using Spring Data JPA or Hibernate
  • Architects designing persistence layers for Kotlin microservices
  • Code reviewers checking entity design and ORM correctness

kotlin-backend-jpa-entity-mapping FAQ

Why can't I use data class for JPA entities?

data class auto-generates equals/hashCode from all fields. When Hibernate modifies an entity's state, its hash changes, breaking Set/Map membership. Also, data class copy() creates detached duplicates, and proxies fail equals checks because lazy fields are uninitialized.

How do I handle DB-generated IDs safely?

Model the ID as nullable (var id: Long? = null) with a protected setter. Never use 0L as a sentinel. This way, unsaved entities have id=null, and equals/hashCode can safely check id != null before comparing.

What's the difference between orphanRemoval and cascade remove?

orphanRemoval deletes child entities when they're removed from a collection; cascade remove deletes them when the parent is deleted. They have different lifecycle semantics—choose based on whether children can exist independently.

How do I fix N+1 queries?

Diagnose by counting actual SQL queries in logs. Then apply targeted solutions: @EntityGraph for single queries, JOIN FETCH for explicit joins, batch fetching for collections, or DTO projection to avoid loading full entities.

Why does my entity fail in JSON serialization?

JSON serializers often trigger lazy-load exceptions by accessing uninitialized collections. Use DTO projection or @JsonIgnore on lazy fields, or ensure serialization happens inside a transaction boundary.

Full instructions (SKILL.md)

Source of truth, from kotlin/kotlin-agent-skills.


name: kotlin-backend-jpa-entity-mapping description: > 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. license: Apache-2.0 metadata: author: JetBrains version: "1.0.0"

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

// WRONG: data class generates equals/hashCode from ALL fields,
// and the generated ID uses a 0 sentinel instead of null
data 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

@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

@Service
class 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

@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?
}

@Service
class 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.