kotlin-backend-jpa-entity-mapping
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseJPA Entity Mapping for Kotlin
Kotlin的JPA实体映射
Kotlin's is natural for DTOs but dangerous for JPA entities. Hibernate relies on
identity semantics that breaks: / over all fields corrupts
/ membership after state changes, and auto-generated creates detached
duplicates of managed entities.
data classdata classequalshashCodeSetMapcopy()This skill teaches correct entity design, identity strategies, and uniqueness constraints
for Kotlin + Spring Data JPA projects.
Kotlin的非常适合作为DTO,但用于JPA实体则存在风险。Hibernate依赖的标识语义会被破坏:基于所有字段生成的/会在状态变更后导致/的成员关系失效,而自动生成的方法会创建托管实体的游离副本。
data classdata classequalshashCodeSetMapcopy()本内容将教授Kotlin + Spring Data JPA项目中正确的实体设计、标识策略和唯一性约束。
Entity Design Rules
实体设计规则
- Never use for JPA entities. Use a regular
data class. Keepclassfor DTOs.data class - 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 only when the project already accepts that tradeoff and the lifecycle is safe.
lateinit - Verify or equivalent no-arg support when JPA entities exist.
kotlin("plugin.jpa") - Verify classes and members are compatible with proxying where needed.
- 切勿将用于JPA实体。请使用普通
data class。class仅用于DTO。data class - 除非项目明确使用共享模型,否则请将传输DTO与持久化实体分开。
- 仅当对象构造和持久化生命周期确保安全时,才将必填列建模为非空类型。
- 仅当项目已接受相关权衡且生命周期安全时,才使用。
lateinit - 当存在JPA实体时,请验证是否启用了或等效的无参构造函数支持。
kotlin("plugin.jpa") - 验证类和成员是否与所需的代理机制兼容。
Identity and Equality
标识与相等性
- Never accept all-field /
equalsgenerated byhashCodeon an entity.data class - Follow project conventions when they already define an identity strategy.
- If no convention exists, use ID-based equality with a stable .
hashCode - Be explicit about mutable fields and lazy associations when discussing equality.
- 切勿在实体上使用生成的全字段
data class/equals方法。hashCode - 如果项目已定义标识策略,请遵循项目约定。
- 如果没有约定,请使用基于ID的相等性策略并生成稳定的。
hashCode - 在讨论相等性时,需明确说明可变字段和延迟关联。
Broken: data class
Entity
data class错误示例:使用data class
作为实体
data classkotlin
// WRONG: data class generates equals/hashCode from ALL fields
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)kotlin
// WRONG: data class generates equals/hashCode from ALL fields
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
正确示例:基于ID标识的普通类
kotlin
@Entity
@Table(name = "orders")
class Order(
@Column(nullable = false)
var status: String,
@Column(nullable = false)
var total: BigDecimal
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Order) return false
return id != 0L && 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:
- compares by ID only — stable under dirty tracking and proxy unwrapping
equals - returns class-based constant — avoids
hashCode/Setcorruption after persistMap - excludes lazy-loaded relations — prevents
toStringLazyInitializationException - Constructor params are mutable entity fields; is
idwith defaultval
kotlin
@Entity
@Table(name = "orders")
class Order(
@Column(nullable = false)
var status: String,
@Column(nullable = false)
var total: BigDecimal
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Order) return false
return id != 0L && id == other.id
}
override fun hashCode(): Int = javaClass.hashCode()
// toString must NOT reference lazy collections
override fun toString(): String = "Order(id=$id, status=$status)"
}核心规则:
- 仅通过ID进行比较 —— 在脏数据跟踪和代理解包时保持稳定
equals - 返回基于类的常量 —— 避免持久化后
hashCode/Set出现数据损坏Map - 排除延迟加载的关联关系 —— 防止出现
toStringLazyInitializationException - 构造函数参数为可变实体字段;为带默认值的
idval
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.
当API需要实现幂等性时(例如“为订单X预留库存”),需在两层都强制执行唯一性:数据库约束确保正确性,应用层检查提供清晰的错误信息。
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))
}
}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)
val id: Long = 0
}
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 queries automatically
findByXAndY
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)
val id: Long = 0
}
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))
}
}核心规则:
- 数据库约束是必须的 —— 仅应用层检查存在竞态条件
- 应用层检查提供清晰的错误信息 —— 否则用户会收到原始的异常
DataIntegrityViolationException - 两层结合:应用层处理常见情况,数据库层处理竞态场景
- Spring Data会自动推导查询
findByXAndY
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, batch fetching, or DTO projection.JOIN FETCH - Be careful with collection fetch joins plus pagination — call out the tradeoff.
- Use indexes and uniqueness constraints to support real query patterns.
- 通过实际查询数量或SQL日志诊断N+1问题,而非仅通过注解猜测。
- 优先使用针对性的抓取方案:、
@EntityGraph、批量抓取或DTO投影。JOIN FETCH - 需注意集合抓取关联加上分页的场景 —— 明确说明权衡点。
- 使用索引和唯一性约束支持实际的查询模式。
Common ORM Traps
常见ORM陷阱
- Bidirectional associations: maintain both sides in domain methods. Half-updated graphs cause subtle bugs.
- vs cascade remove: not interchangeable. Explain lifecycle semantics before choosing.
orphanRemoval - Lazy load triggers: , debug logging, JSON serialization, and IDE inspection can all trigger lazy loads.
toString - 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.
- + mutable equality: collection membership can break after entity state changes.
Set - : the clearest optimistic concurrency mechanism when concurrent updates matter.
@Version - disabled: DTO mapping touching lazy fields must happen inside a transaction boundary.
open-in-view
- 双向关联: 在领域方法中维护关联的双方。更新不完整的对象图会导致隐蔽的Bug。
- 与级联删除: 二者不可互换。选择前需明确生命周期语义。
orphanRemoval - 延迟加载触发因素: 、调试日志、JSON序列化和IDE检查都可能触发延迟加载。
toString - 批量更新/删除: 会绕过持久化上下文和生命周期回调。后续读取可能出现脏数据。
- 多集合抓取: 可能导致笛卡尔积爆炸。需验证ORM能否安全执行集合密集型的抓取计划。
- + 可变相等性: 实体状态变更后,集合成员关系可能失效。
Set - : 当存在并发更新时,这是最清晰的乐观锁机制。
@Version - 禁用: 涉及延迟字段的DTO映射必须在事务边界内完成。
open-in-view
Guardrails
防护准则
- Do not use for JPA entities.
data class - Do not recommend everywhere to silence lazy loading symptoms.
FetchType.EAGER - 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.
- 切勿将用于JPA实体。
data class - 切勿推荐全局使用来掩盖延迟加载的问题。
FetchType.EAGER - 默认情况下,切勿直接通过API响应暴露实体。
- 切勿在未解释抓取计划如何改变查询行为的情况下,声称已修复N+1问题。