Skip to main content
ClaudeWave
Skill262 estrellas del repoactualizado 5d ago

spring-data-jpa

Spring Data JPA configures entity mappings and repository queries for relational databases using conventions like UUID primary keys, STRING-based enums, protected no-argument constructors, and Lombok annotations. Use this skill when building Spring Boot applications that require JPA entity definitions with N+1 query prevention through JOIN FETCH and projection patterns for efficient database access.

Instalar en Claude Code
Copiar
git clone --depth 1 https://github.com/rrezartprebreza/spring-boot-skills /tmp/spring-data-jpa && cp -r /tmp/spring-data-jpa/skills/spring-boot-4/spring-data-jpa ~/.claude/skills/spring-data-jpa
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.md

# Spring Data JPA (Boot 4 / Hibernate 7)

Spring Boot 4 manages Jakarta Persistence 3.2, Jakarta Validation 3.1, and Hibernate ORM 7.x. Use
Boot dependency management and import `jakarta.persistence.*` / `jakarta.validation.*`. Do not add
explicit Hibernate, JPA, or Validator versions unless the project has a deliberate override policy.

## Entity Model Rules

Use an `@Entity` only for persistent state with identity and lifecycle. Use records for DTOs,
commands, and read models. Use `@Embeddable` for values stored inside an entity table.

```java
@Entity
@Table(name = "orders", indexes = {
    @Index(name = "idx_orders_customer_id", columnList = "customer_id"),
    @Index(name = "idx_orders_status_created", columnList = "status, created_at")
})
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(nullable = false, updatable = false)
    private UUID id;

    @Version
    private Long version;

    @Column(name = "customer_id", nullable = false, updatable = false)
    private UUID customerId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 32)
    private OrderStatus status;

    @Embedded
    private Money total;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;

    public static Order create(UUID customerId) {
        Order order = new Order();
        order.customerId = Objects.requireNonNull(customerId);
        order.status = OrderStatus.DRAFT;
        order.total = Money.zero("EUR");
        return order;
    }

    public void addItem(UUID productId, int quantity, Money unitPrice) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot edit submitted order");
        }
        items.add(OrderItem.create(this, productId, quantity, unitPrice));
        recalculateTotal();
    }

    private void recalculateTotal() {
        total = items.stream()
            .map(OrderItem::subtotal)
            .reduce(Money.zero("EUR"), Money::add);
    }
}
```

Rules:

- Use `jakarta.persistence.*`, never `javax.persistence.*`.
- Keep entities non-final with a protected no-arg constructor so Hibernate can instantiate/proxy them.
- Do not use Java records for ordinary entities. Records are good DTOs and sometimes embeddables.
- Use targeted Lombok (`@Getter`, protected `@NoArgsConstructor`), not `@Data` or broad `@Setter`.
- Prefer behavior methods and static factories over public setters/constructors.
- Initialize collections inline. JPA collection fields should not be null.
- Use `@Enumerated(EnumType.STRING)` with explicit column length. Never use `ORDINAL`.
- Add `@Version Long version` for user-editable aggregates. Use wrapper `Long`, not primitive `long`.
- Prefer `UUID` or pooled sequence IDs. Avoid `GenerationType.IDENTITY` on high-write tables because
  it disables insert batching.
- Validate request DTOs at the boundary; enforce entity invariants inside behavior methods.

## Embeddables and DTOs

```java
@Embeddable
public record Money(
    @Column(name = "amount", nullable = false, precision = 19, scale = 2)
    BigDecimal amount,

    @Column(name = "currency", nullable = false, length = 3)
    String currency
) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        if (amount.signum() < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
    }

    public static Money zero(String currency) {
        return new Money(BigDecimal.ZERO, currency);
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount.add(other.amount), currency);
    }

    public Money multiply(int quantity) {
        if (quantity < 1) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
    }
}
```

Never expose entities from controllers. Map entities to response records:

```java
public record OrderResponse(UUID id, String status, BigDecimal total, Instant createdAt) {
    static OrderResponse from(Order order) {
        return new OrderResponse(
            order.getId(),
            order.getStatus().name(),
            order.getTotal().amount(),
            order.getCreatedAt());
    }
}
```

## Relationships

Map the database shape first. Prefer normal foreign keys: `@ManyToOne` on the owning side and
`@OneToMany(mappedBy = ...)` only when parent-to-child navigation is actually needed.

```java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "order_id", nullable = false, foreignKey = @ForeignKey(name = "fk_order_item_order"))
    private Order order;

    @Column(name = "product_id", nullable = false, updatable = false)
    private UUID productId;

    private int quantity;
    private Money unitPrice;

    static OrderItem create(Order order, UUID productId, int quantity, Money unitPrice) {
        OrderItem item = new OrderItem();
        item.order = Objects.requireNonNull(order);
        item.productId = Objects.requireNonNull(productId);
        item.quantity = quantity;
        item.unitPrice = Objects.requireNonNull(unitPrice);
        return item;
    }

    Money subtotal() {
        return unitPrice.multiply(quantity);
    }
}
```

- Put `fetch = FetchType.LAZY` on `@ManyToOne