$npx -y skills add rrezartprebreza/spring-boot-skills --skill spring-data-jpaUse when generating JPA entities, repositories, queries, or anything touching the persistence layer. Covers entity conventions, N+1 prevention, projections, and query patterns.
| 1 | # Spring Data JPA |
| 2 | |
| 3 | ## Entity Conventions |
| 4 | |
| 5 | ```java |
| 6 | @Entity |
| 7 | @Table(name = "orders") |
| 8 | @Getter |
| 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requires no-arg, hide from callers |
| 10 | public class Order { |
| 11 | |
| 12 | @Id |
| 13 | @GeneratedValue(strategy = GenerationType.UUID) |
| 14 | @Column(updatable = false, nullable = false) |
| 15 | private UUID id; |
| 16 | |
| 17 | @Column(nullable = false) |
| 18 | private String customerEmail; |
| 19 | |
| 20 | @Enumerated(EnumType.STRING) // always STRING, never ORDINAL |
| 21 | @Column(nullable = false) |
| 22 | private OrderStatus status; |
| 23 | |
| 24 | @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) |
| 25 | private List<OrderItem> items = new ArrayList<>(); |
| 26 | |
| 27 | @CreationTimestamp |
| 28 | @Column(updatable = false) |
| 29 | private Instant createdAt; |
| 30 | |
| 31 | @UpdateTimestamp |
| 32 | private Instant updatedAt; |
| 33 | |
| 34 | // Static factory, not public constructor |
| 35 | public static Order create(String customerEmail) { |
| 36 | Order order = new Order(); |
| 37 | order.customerEmail = customerEmail; |
| 38 | order.status = OrderStatus.PENDING; |
| 39 | return order; |
| 40 | } |
| 41 | |
| 42 | // Behavior on entity, not in service |
| 43 | public void addItem(Product product, int quantity) { |
| 44 | items.add(OrderItem.create(this, product, quantity)); |
| 45 | } |
| 46 | } |
| 47 | ``` |
| 48 | |
| 49 | ## Rules |
| 50 | - `@Enumerated(EnumType.STRING)` always — `ORDINAL` breaks on enum reordering |
| 51 | - `GenerationType.UUID` for IDs — never expose auto-increment integers |
| 52 | - `@NoArgsConstructor(access = PROTECTED)` — required by JPA, hidden from app code |
| 53 | - `@Getter` from Lombok — no `@Setter` on entities (use behavior methods) |
| 54 | - Collections initialized inline (`= new ArrayList<>()`) — never null |
| 55 | |
| 56 | ## N+1 Prevention |
| 57 | |
| 58 | **Identify:** One query for orders + N queries for each order's items = N+1. |
| 59 | |
| 60 | **Fix with JOIN FETCH:** |
| 61 | ```java |
| 62 | @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id") |
| 63 | Optional<Order> findByIdWithItems(@Param("id") UUID id); |
| 64 | |
| 65 | // For lists — use @EntityGraph to avoid duplicates |
| 66 | @EntityGraph(attributePaths = {"items", "items.product"}) |
| 67 | List<Order> findByStatus(OrderStatus status); |
| 68 | ``` |
| 69 | |
| 70 | **Fix with Projections for read-only views:** |
| 71 | ```java |
| 72 | // Interface projection — no entity loaded |
| 73 | public interface OrderSummary { |
| 74 | UUID getId(); |
| 75 | String getCustomerEmail(); |
| 76 | OrderStatus getStatus(); |
| 77 | Instant getCreatedAt(); |
| 78 | } |
| 79 | |
| 80 | List<OrderSummary> findByStatus(OrderStatus status); // fast, no lazy loading issues |
| 81 | ``` |
| 82 | |
| 83 | ## Query Patterns |
| 84 | |
| 85 | ```java |
| 86 | public interface OrderRepository extends JpaRepository<Order, UUID> { |
| 87 | |
| 88 | // Derived query — simple conditions |
| 89 | List<Order> findByStatusAndCustomerEmail(OrderStatus status, String email); |
| 90 | |
| 91 | // JPQL — for joins and complex conditions |
| 92 | @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status") |
| 93 | List<Order> findActiveOrdersWithItems(@Param("status") OrderStatus status); |
| 94 | |
| 95 | // Native SQL — only when JPQL can't do it |
| 96 | @Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'", |
| 97 | nativeQuery = true) |
| 98 | List<Order> findRecentOrders(); |
| 99 | |
| 100 | // Exists check — faster than findById + isPresent |
| 101 | boolean existsByCustomerEmailAndStatus(String email, OrderStatus status); |
| 102 | |
| 103 | // Projection |
| 104 | List<OrderSummary> findByCustomerEmail(String email); |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | ## Pagination |
| 109 | |
| 110 | ```java |
| 111 | // Always use Pageable for list endpoints |
| 112 | Page<Order> findByStatus(OrderStatus status, Pageable pageable); |
| 113 | |
| 114 | // In service |
| 115 | Page<Order> orders = orderRepository.findByStatus(status, PageRequest.of(page, size, Sort.by("createdAt").descending())); |
| 116 | ``` |
| 117 | |
| 118 | ## Bidirectional Relationships |
| 119 | |
| 120 | ```java |
| 121 | // Parent side (Order) |
| 122 | @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) |
| 123 | private List<OrderItem> items = new ArrayList<>(); |
| 124 | |
| 125 | // Child side (OrderItem) — owns the FK |
| 126 | @ManyToOne(fetch = FetchType.LAZY) // LAZY always on @ManyToOne |
| 127 | @JoinColumn(name = "order_id", nullable = false) |
| 128 | private Order order; |
| 129 | |
| 130 | // Helper on parent to keep both sides in sync |
| 131 | public void addItem(OrderItem item) { |
| 132 | items.add(item); |
| 133 | item.setOrder(this); |
| 134 | } |
| 135 | ``` |
| 136 | |
| 137 | ## Deep Pagination — Keyset over OFFSET |
| 138 | |
| 139 | `OFFSET` pagination scans and discards every skipped row. On page 5,000 the DB reads 100,000 rows to |
| 140 | return 20. For large or infinite-scroll datasets, paginate by the last seen key (the "seek" method): |
| 141 | |
| 142 | ```java |
| 143 | // ❌ Slow on deep pages — OFFSET grows linearly |
| 144 | Page<Order> findByStatus(OrderStatus status, Pageable pageable); |
| 145 | |
| 146 | // ✅ Keyset — constant time regardless of depth. Pass the last row's createdAt + id. |
| 147 | @Query(""" |
| 148 | SELECT o FROM Order o |
| 149 | WHERE o.status = :status |
| 150 | AND (o.createdAt < :lastCreatedAt |
| 151 | OR (o.createdAt = :lastCreatedAt AND o.id < :lastId)) |
| 152 | ORDER BY o.createdAt DESC, o.id DESC |
| 153 | """) |
| 154 | List<Order> findNextPage(OrderStatus status, Instant lastCreatedAt, UUID lastId, Limit limit); |
| 155 | ``` |
| 156 | |
| 157 | The `( |