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- 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
How to use kotlin-backend-jpa-entity-mapping
- 1.Replace any data class entities with regular classes and implement ID-based equals/hashCode
- 2.Verify entity constructors accept mutable fields and DB-generated IDs are nullable with protected setters
- 3.Add @UniqueConstraint annotations to @Table for business-critical uniqueness rules
- 4.Implement application-level guards (repository queries) before save() to catch duplicates early
- 5.Use @EntityGraph or JOIN FETCH to eliminate N+1 queries; verify with SQL logs or query counts
- 6.Exclude lazy-loaded collections from toString() and equals() to prevent LazyInitializationException
Use cases
- 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
- 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
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.
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.
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.
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.
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 classfor JPA entities. Use a regularclass. Keepdata classfor 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
lateinitonly 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/hashCodegenerated bydata classon 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? = nulland aprotected set; do not use0Las 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:
equalscompares by ID only — stable under dirty tracking and proxy unwrappinghashCodereturns class-based constant — avoidsSet/Mapcorruption after persisttoStringexcludes lazy-loaded relations — preventsLazyInitializationException- Constructor params are mutable entity fields; DB-generated
idis 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
findByXAndYqueries 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.
orphanRemovalvs 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-viewdisabled: DTO mapping touching lazy fields must happen inside a transaction boundary.
Guardrails
- Do not use
data classfor JPA entities. - Do not recommend
FetchType.EAGEReverywhere 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.
Related skills
More from kotlin/kotlin-agent-skills and the wider catalog.

kotlin-tooling-agp9-migration
Migrate Kotlin Multiplatform projects to Android Gradle Plugin 9.0+

kotlin-tooling-cocoapods-spm-migration
Migrate Kotlin Multiplatform projects from CocoaPods to Swift Package Manager dependencies.

kotlin-tooling-java-to-kotlin
Convert Java source files to idiomatic Kotlin with framework-aware guidance and 4-step methodology.

byted-ark-seedream-skill
豆包 Seedream AI 图片生成 Skill - 火山方舟 Agent Plan 专属版本,支持连贯组图、流式输出、提示词优化及联网搜索。

byted-web-search
Volcano Engine web search API with 500 free monthly queries for real-time internet access.

vision
Delegate image understanding to a configurable OpenAI-compatible vision model when you lack native vision.