Skip to content

/kotlin-backend-jpa-entity-mapping

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

From plugin
kotlin-agent-skills
1k10 skills
Install
$ npx -y skills add kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/kotlin-backend-jpa-entity-mapping

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

kotlin-backend-jpa-entity-mapping.SKILL.md
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 fetc
Read more
Ships withkotlin-agent-skills

A collection of AI agent skills useful for projects using the Kotlin language. Skills are following the Agent Skills standard, see agentskills.io for more information.

Get the whole plugin, auto-invoked

Other skills on kotlin-agent-skills.