refactor(page): 删除旧版页面文件
This commit is contained in:
185
.agents/skills/kotlin-backend-jpa-entity-mapping/SKILL.md
Normal file
185
.agents/skills/kotlin-backend-jpa-entity-mapping/SKILL.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user