npx skills add ...
npx skills add kotlin/kotlin-agent-skills --skill kotlin-backend-jpa-entity-mapping
npx skills add kotlin/kotlin-agent-skills --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 (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.
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.
data class for JPA entities. Use a regular class. Keep data class for DTOs.lateinit only when the project already accepts that tradeoff and the lifecycle is safe.kotlin("plugin.jpa") or equivalent no-arg support when JPA entities exist.equals/hashCode generated by data class on an entity.hashCode.var id: Long? = null
and a protected set; do not use 0L as a sentinel value.data class EntityKey rules:
equals compares by ID only — stable under dirty tracking and proxy unwrappinghashCode returns class-based constant — avoids Set/Map corruption after persisttoString excludes lazy-loaded relations — prevents LazyInitializationExceptionid is nullable with a protected setterWhen 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.
Key rules:
DataIntegrityViolationExceptionfindByXAndY queries automatically@EntityGraph, JOIN FETCH, batch fetching, or DTO projection.orphanRemoval vs cascade remove: not interchangeable. Explain lifecycle semantics before choosing.toString, debug logging, JSON serialization, and IDE inspection can all trigger lazy loads.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.data class for JPA entities.FetchType.EAGER everywhere to silence lazy loading symptoms.@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)"
}@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))
}
}@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))
}
}