$npx -y skills add ducpm2303/claude-java-plugins --skill java-jpaReviews Spring Data JPA for N+1 queries, fetch strategies, projections, and Specifications. Use when user asks to "review JPA", "check for N+1", "JPA performance", "review my entities", "check fetch strategy", or "review my repositories".
| 1 | # /java-jpa — Spring Data JPA Deep Review |
| 2 | |
| 3 | You are a Spring Data JPA specialist. Review JPA code for common performance and correctness problems. |
| 4 | |
| 5 | ## Step 1 — Detect Java and Spring Boot version |
| 6 | |
| 7 | Check `pom.xml` or `build.gradle` for: |
| 8 | - Java version (affects stream/record usage) |
| 9 | - Spring Boot version (2.x uses `javax.persistence.*`, 3.x uses `jakarta.persistence.*`) |
| 10 | |
| 11 | If neither is found, ask the user. |
| 12 | |
| 13 | ## Step 2 — Identify scope |
| 14 | |
| 15 | If the user provided a file or class name, focus on that. Otherwise, scan for: |
| 16 | - `@Entity`, `@Repository`, `@Service` classes |
| 17 | - `JpaRepository` / `CrudRepository` / `PagingAndSortingRepository` extensions |
| 18 | - JPQL and native queries (`@Query`) |
| 19 | - `@Transactional` annotations |
| 20 | |
| 21 | ## Step 3 — Review for these issues |
| 22 | |
| 23 | ### N+1 Query Detection (HIGH PRIORITY) |
| 24 | Look for: |
| 25 | - `@OneToMany` or `@ManyToMany` without `fetch = FetchType.LAZY` (lazy is required; eager causes N+1 on collections) |
| 26 | - Loops that call a repository method or access a lazy collection on each iteration |
| 27 | - Missing `@EntityGraph` or JOIN FETCH when a relationship is always needed together |
| 28 | |
| 29 | For each N+1 found, show: |
| 30 | ``` |
| 31 | ⚠️ N+1 DETECTED: UserService.getUsersWithOrders() |
| 32 | Problem: orders collection loaded lazily inside a loop (line 42) |
| 33 | Fix: |
| 34 | Option A — @EntityGraph on the repository method: |
| 35 | @EntityGraph(attributePaths = {"orders"}) |
| 36 | List<User> findAll(); |
| 37 | |
| 38 | Option B — JOIN FETCH in JPQL: |
| 39 | @Query("SELECT u FROM User u JOIN FETCH u.orders") |
| 40 | List<User> findAllWithOrders(); |
| 41 | ``` |
| 42 | |
| 43 | ### Fetch Strategy Review |
| 44 | - Flag `FetchType.EAGER` on `@OneToMany` / `@ManyToMany` — always wrong |
| 45 | - Flag missing `@BatchSize` when lazy collections will be accessed in loops |
| 46 | - Suggest `@EntityGraph` for use-case-specific eager loading without global EAGER |
| 47 | |
| 48 | ### Projection Usage |
| 49 | Check if entities are returned where only a subset of fields is needed: |
| 50 | ``` |
| 51 | 💡 PROJECTION OPPORTUNITY: UserRepository.findAllForList() |
| 52 | Returns full User entity but only id, name, email are used in UserListDto. |
| 53 | Fix: Use an interface projection or record projection (Java 16+): |
| 54 | |
| 55 | // Interface projection (Java 8+) |
| 56 | public interface UserSummary { |
| 57 | Long getId(); |
| 58 | String getName(); |
| 59 | String getEmail(); |
| 60 | } |
| 61 | List<UserSummary> findAllBy(); |
| 62 | |
| 63 | // Record projection (Java 16+, Spring Boot 2.6+) |
| 64 | public record UserSummary(Long id, String name, String email) {} |
| 65 | @Query("SELECT new com.example.UserSummary(u.id, u.name, u.email) FROM User u") |
| 66 | List<UserSummary> findAllSummaries(); |
| 67 | ``` |
| 68 | |
| 69 | ### Specification / Criteria API |
| 70 | If dynamic queries are constructed with string concatenation or if/else JPQL building: |
| 71 | ``` |
| 72 | 💡 SPECIFICATION OPPORTUNITY: ProductRepository |
| 73 | Dynamic query built with string concatenation is brittle and not type-safe. |
| 74 | Fix: Use JpaSpecificationExecutor<Product> + Specification<Product>: |
| 75 | |
| 76 | public static Specification<Product> hasCategory(String category) { |
| 77 | return (root, query, cb) -> |
| 78 | category == null ? null : cb.equal(root.get("category"), category); |
| 79 | } |
| 80 | public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) { |
| 81 | return (root, query, cb) -> |
| 82 | cb.between(root.get("price"), min, max); |
| 83 | } |
| 84 | // Usage: |
| 85 | repo.findAll(hasCategory(cat).and(priceBetween(min, max))); |
| 86 | ``` |
| 87 | |
| 88 | ### Transaction Boundaries |
| 89 | - Flag `@Transactional` on controller methods — belongs at service layer only |
| 90 | - Flag `@Transactional(readOnly = true)` missing on read-only service methods (enables Hibernate optimizations) |
| 91 | - Flag calling a `@Transactional` method from within the same bean (self-invocation bypasses proxy) |
| 92 | - Flag long transactions that include external HTTP calls or file I/O |
| 93 | |
| 94 | ### Entity Design |
| 95 | - Flag mutable `List`/`Set` fields without `@Column(nullable = false)` / proper constraints |
| 96 | - Flag missing `equals()` and `hashCode()` on entities used in Sets or as Map keys |
| 97 | - Flag `@GeneratedValue(strategy = GenerationType.AUTO)` — prefer `IDENTITY` (MySQL/PostgreSQL) or `SEQUENCE` for performance |
| 98 | - Flag bidirectional relationships without the owning side's `mappedBy` set correctly |
| 99 | |
| 100 | ### Pagination |
| 101 | - Flag `findAll()` without `Pageable` when the table could be large |
| 102 | - Suggest `Page<T>` vs `Slice<T>` (use Slice when total count is not needed — avoids COUNT query) |
| 103 | |
| 104 | ### Native Queries |
| 105 | - Flag native `@Query` with `nativeQuery = true` when JPQL would suffice |
| 106 | - Flag string-interpolated table/column names in native queries (SQL injection risk) |
| 107 | |
| 108 | ## Step 4 — Output |
| 109 | |
| 110 | Produce a structured report: |
| 111 | |
| 112 | ``` |
| 113 | ## JPA Review — [ClassName or scope] |
| 114 | |
| 115 | ### Critical Issues |
| 116 | [N+1 problems, missing transactions on writes] |
| 117 | |
| 118 | ### Performance Improvements |
| 119 | [Fetch strategies, projection |