Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when generating JPA entities, repositories, queries, or anything touching the persistence layer. Covers entity conventions, N+1 prevention, projections, and query patterns.
.claude/skills/rrezartprebreza-spring-data-jpa/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 111% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 104% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 87% | 0% |
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.
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:
jakarta.persistence.*, never javax.persistence.*.@Getter, protected @NoArgsConstructor), not @Data or broad @Setter.@Enumerated(EnumType.STRING) with explicit column length. Never use ORDINAL.@Version Long version for user-editable aggregates. Use wrapper Long, not primitive long.UUID or pooled sequence IDs. Avoid GenerationType.IDENTITY on high-write tables becauseit disables insert batching.
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:
javapublic 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()); } }
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); } }
fetch = FetchType.LAZY on @ManyToOne and @OneToOne; to-one mappings are eager by default.orphanRemoval = true only when the parent truly owns the child's lifecycle.@ManyToMany for business relationships with attributes; model the join row as an entity.Do not generate entity equality with Lombok @Data. It includes mutable fields and associations, which can trigger lazy loading, recursion, and hash changes.
Preferred options:
constraint.
proxy-safe generated-ID pattern.
equals, hashCode, or toString.instanceof, not getClass(), when equality must work with Hibernate proxies.java@Override public boolean equals(Object other) { return other instanceof Customer that && email != null && email.equals(that.getEmail()); } @Override public int hashCode() { return email == null ? 0 : email.hashCode(); }
javapublic interface OrderRepository extends JpaRepository<Order, UUID> { boolean existsByCustomerIdAndStatus(UUID customerId, OrderStatus status); Optional<Order> findByIdAndCustomerId(UUID id, UUID customerId); @EntityGraph(attributePaths = {"items"}) Optional<Order> findById(UUID id); @Query(""" select o from Order o where o.status = :status order by o.createdAt desc, o.id desc """) List<Order> findRecentByStatus(OrderStatus status, Limit limit); }
Use:
@Query for explicit joins, keyset pagination, and complex predicates.@EntityGraph for bounded graph loading.exists... queries instead of find...().isPresent() checks.Avoid:
findAll() in endpoints.Identify N+1 by looking for lazy association access inside loops or JSON serialization of entities.
java@EntityGraph(attributePaths = {"items", "items.product"}) Optional<Order> findWithItemsAndProductsById(UUID id); public interface OrderSummary { UUID getId(); UUID getCustomerId(); OrderStatus getStatus(); Instant getCreatedAt(); } List<OrderSummary> findByStatus(OrderStatus status);
Use fetch joins and entity graphs only for bounded relationships. For list endpoints, prefer projections to avoid loading entire aggregate graphs.
Use Pageable for normal list screens:
javaPage<Order> findByStatus(OrderStatus status, Pageable pageable);
Use keyset pagination for deep or infinite-scroll lists. OFFSET pagination scans and discards skipped rows.
java@Query(""" select o from Order o where o.status = :status and (o.createdAt < :lastCreatedAt or (o.createdAt = :lastCreatedAt and o.id < :lastId)) order by o.createdAt desc, o.id desc """) List<Order> findNextPage(OrderStatus status, Instant lastCreatedAt, UUID lastId, Limit limit);
The (createdAt, id) tuple keeps the cursor stable when timestamps collide. Back it with an index like (status, created_at desc, id desc).
Enable JDBC batching for write-heavy workloads:
yamlspring: jpa: properties: hibernate: jdbc.batch_size: 50 order_inserts: true order_updates: true
GenerationType.IDENTITY disables insert batching because Hibernate needs the generated key after each row. Use UUIDs or pooled sequences when batch insert throughput matters.
Spring Data JPA detects new entities by nullable wrapper @Version first, then nullable ID. A primitive version cannot be used because JPA treats 0 as the first persisted version.
For manually assigned IDs, add @Version Long version or implement Persistable with an isNew flag cleared by @PostPersist and @PostLoad. Use the template in templates/BaseAssignedIdEntity.java.
javax.persistence.* - Boot 4 uses jakarta.persistence.*.@Data on entities - generates setters and unsafe equality; use targeted @Getter.final or constructors private - breaks Hibernate proxy/instantiation.FetchType.EAGER - use LAZY on to-one and many-to-many relationships.@Enumerated(EnumType.ORDINAL) - use STRING.long version - use nullable wrapper Long.@Version on editable aggregates - lost updates are not detected.findAll() for list endpoints - require Pageable, Limit, or a projection query.OFFSET pagination on huge tables - switch to keyset for deep pages.toString - causes lazy loads and recursion.@ManyToMany for business links with attributes - model the join row as an entity.GenerationType.IDENTITY - batching is silently off; use UUID/sequence.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 17,235 | 15,013 | -13% | 1 | 1 | 0% | 1,837 | 4,374 | +138% | 0 | 0 | — |
case-19 | pass→pass | 13,280 | 7,377 | -44% | 1 | 1 | 0% | 1,980 | 3,950 | +99% | 0 | 0 | — |
case-03 | fail→fail | 22,595 | 23,609 | +4% | 1 | 1 | 0% | 4,114 | 6,771 | +65% | 0 | 0 | — |
case-04 | fail→pass | 23,839 | 20,198 | -15% | 1 | 1 | 0% | 2,878 | 6,071 | +111% | 0 | 0 | — |
case-01 | fail→pass | 17,725 | 21,860 | +23% | 1 | 1 | 0% | 3,498 | 6,674 | +91% | 0 | 0 | — |
case-02 | fail→pass | 19,419 | 18,262 | -6% | 1 | 1 | 0% | 2,909 | 4,959 | +70% | 0 | 0 | — |
case-06 | fail→pass | 11,226 | 17,220 | +53% | 1 | 1 | 0% | 2,307 | 4,715 | +104% | 0 | 0 | — |
case-07 | fail→pass | 18,065 | 15,400 | -15% | 1 | 1 | 0% | 2,763 | 5,177 | +87% | 0 | 0 | — |
case-08 | pass→pass | 18,134 | 31,721 | +75% | 1 | 1 | 0% | 2,559 | 7,145 | +179% | 0 | 0 | — |
case-09 | fail→fail | 23,626 | 15,462 | -35% | 1 | 1 | 0% | 4,251 | 5,963 | +40% | 0 | 0 | — |
case-10 | pass→pass | 15,806 | 16,972 | +7% | 1 | 1 | 0% | 2,030 | 5,642 | +178% | 0 | 0 | — |
case-11 | fail→pass | 17,269 | 16,079 | -7% | 1 | 1 | 0% | 2,231 | 5,156 | +131% | 0 | 0 | — |
case-12 | pass→pass | 14,333 | 11,643 | -19% | 1 | 1 | 0% | 1,822 | 4,839 | +166% | 0 | 0 | — |
case-13 | fail→pass | 35,736 | 22,185 | -38% | 1 | 1 | 0% | 1,557 | 5,370 | +245% | 0 | 0 | — |
case-14 | pass→pass | 16,473 | 19,876 | +21% | 1 | 1 | 0% | 2,423 | 5,430 | +124% | 0 | 0 | — |
case-15 | fail→pass | 17,701 | 17,588 | -1% | 1 | 1 | 0% | 2,408 | 5,565 | +131% | 0 | 0 | — |
case-16 | pass→pass | 20,580 | 13,247 | -36% | 1 | 1 | 0% | 2,791 | 4,429 | +59% | 0 | 0 | — |
case-17 | pass→pass | 6,629 | 4,366 | -34% | 1 | 1 | 0% | 984 | 3,635 | +269% | 0 | 0 | — |
case-18 | fail→pass | 14,045 | 17,698 | +26% | 1 | 1 | 0% | 2,404 | 4,827 | +101% | 0 | 0 | — |
case-20 | pass→pass | 10,297 | 6,138 | -40% | 1 | 1 | 0% | 1,790 | 4,054 | +126% | 0 | 0 | — |
case-21 | pass→pass | 5,876 | 11,033 | +88% | 1 | 1 | 0% | 1,193 | 5,446 | +356% | 0 | 0 | — |
case-22 | pass→fail | 7,900 | 6,812 | -14% | 1 | 1 | 0% | 1,117 | 3,871 | +247% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +36 percentage points is the difference between those two pass rates over the 21 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.