kotlin-backend-jpa-entity-mapping — Claude Code skill for Kotlin JPA
Teaches your agent to map JPA entities in Kotlin for Spring Data JPA and Hibernate: identity, equals/hashCode, relations, fetch plans.
npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a claude-codeWhat kotlin-backend-jpa-entity-mapping does
A JetBrains skill about the traps you hit when mapping entities in Kotlin. The big one is data class: Hibernate depends on identity semantics, and generated equals/hashCode over every field corrupt Set and Map membership once state changes. The generated copy() also produces detached duplicates of managed entities. The skill wants a plain class for entities and keeps data class for DTOs.
Inside are entity design rules, identity strategies and uniqueness constraints. For database-generated IDs it suggests var id: Long? = null with a protected set rather than a 0L sentinel. equals compares by ID only, hashCode returns a class-based constant, toString stays away from lazy collections. On uniqueness it asks for two layers: a database constraint for correctness and an application check for readable errors — a code check alone leaves a race.
There is a section on N+1 and LazyInitializationException: diagnose from actual query counts or SQL logs, not from annotations. Fixes include @EntityGraph, JOIN FETCH, batch fetching and DTO projections. Then the ORM traps: orphanRemoval and cascade remove are not interchangeable, bulk updates bypass the persistence context, and several collection fetch joins can blow up into a Cartesian product. If your project already has an identity convention, the skill says to follow it instead of imposing another.
What it helps with
- convert a JPA entity from
data classto a regular class - fix
equals/hashCodeso aSetkeeps its elements after persist - track down an N+1 by reading SQL logs
- add a uniqueness constraint plus a clean duplicate error
- keep lazy relations out of
toStringto avoidLazyInitializationException
Example prompts for the agent
- review my
Orderentity and tell me what breaks under Hibernate - why do I get N+1 when loading orders with their line items? rewrite the query
- add a unique constraint on
variant_idandorder_idinReservationand a service-level check
How to install kotlin-backend-jpa-entity-mapping
1Skills CLI (Vercel)
npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a claude-code2Manually: put the files in place
~/.claude/skills/kotlin-backend-jpa-entity-mapping/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/kotlin-backend-jpa-entity-mapping/
1Skills CLI (Vercel)
npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a codex2Manually: put the files in place
.agents/skills/kotlin-backend-jpa-entity-mapping/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a cursor2Manually: put the files in place
.cursor/skills/kotlin-backend-jpa-entity-mapping/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install Kotlin/kotlin-agent-skills kotlin-backend-jpa-entity-mapping2Manually: put the files in place
.github/skills/kotlin-backend-jpa-entity-mapping/SKILL.md1Skills CLI (Vercel)
npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a gemini-cli2Platform CLI
gemini skills install https://github.com/Kotlin/kotlin-agent-skills --path skills/kotlin-backend-jpa-entity-mappingManually: put the files in place
.devin/skills/kotlin-backend-jpa-entity-mapping/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about kotlin-backend-jpa-entity-mapping
How do I install kotlin-backend-jpa-entity-mapping in Claude?
Run npx skills add Kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping -a claude-code. The skill file lands at ~/.claude/skills/kotlin-backend-jpa-entity-mapping/SKILL.md.
What does kotlin-backend-jpa-entity-mapping do?
It gives the agent rules for modelling JPA entities in Kotlin with Spring Data JPA and Hibernate: entity design, identity and equality, uniqueness constraints, relationships and fetch plans.
Why can't a JPA entity be a data class?
Generated equals/hashCode cover all fields, so an entity drops out of a Set or Map after its state changes. copy() also creates a detached copy of a managed entity.
Source text
SKILL.md · Apache-2.0
---
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
```kotlin
// 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
```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
@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
```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?
}
@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.
© JetBrains · Apache-2.0 · Kotlin/kotlin-agent-skills