Skip to content
Development
Skill

/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
9926 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

Other skills on kotlin-agent-skills.