$npx -y skills add decebals/claude-code-java --skill jpa-patternsJPA/Hibernate patterns and common pitfalls (N+1, lazy loading, transactions, queries). Use when user has JPA performance issues, LazyInitializationException, or asks about entity relationships and fetching strategies.
| 1 | # JPA Patterns Skill |
| 2 | |
| 3 | Best practices and common pitfalls for JPA/Hibernate in Spring applications. |
| 4 | |
| 5 | ## When to Use |
| 6 | - User mentions "N+1 problem" / "too many queries" |
| 7 | - LazyInitializationException errors |
| 8 | - Questions about fetch strategies (EAGER vs LAZY) |
| 9 | - Transaction management issues |
| 10 | - Entity relationship design |
| 11 | - Query optimization |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## Quick Reference: Common Problems |
| 16 | |
| 17 | | Problem | Symptom | Solution | |
| 18 | |---------|---------|----------| |
| 19 | | N+1 queries | Many SELECT statements | JOIN FETCH, @EntityGraph | |
| 20 | | LazyInitializationException | Error outside transaction | Open Session in View, DTO projection, JOIN FETCH | |
| 21 | | Slow queries | Performance issues | Pagination, projections, indexes | |
| 22 | | Dirty checking overhead | Slow updates | Read-only transactions, DTOs | |
| 23 | | Lost updates | Concurrent modifications | Optimistic locking (@Version) | |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## N+1 Problem |
| 28 | |
| 29 | > The #1 JPA performance killer |
| 30 | |
| 31 | ### The Problem |
| 32 | |
| 33 | ```java |
| 34 | // ❌ BAD: N+1 queries |
| 35 | @Entity |
| 36 | public class Author { |
| 37 | @Id private Long id; |
| 38 | private String name; |
| 39 | |
| 40 | @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) |
| 41 | private List<Book> books; |
| 42 | } |
| 43 | |
| 44 | // This innocent code... |
| 45 | List<Author> authors = authorRepository.findAll(); // 1 query |
| 46 | for (Author author : authors) { |
| 47 | System.out.println(author.getBooks().size()); // N queries! |
| 48 | } |
| 49 | // Result: 1 + N queries (if 100 authors = 101 queries) |
| 50 | ``` |
| 51 | |
| 52 | ### Solution 1: JOIN FETCH (JPQL) |
| 53 | |
| 54 | ```java |
| 55 | // ✅ GOOD: Single query with JOIN FETCH |
| 56 | public interface AuthorRepository extends JpaRepository<Author, Long> { |
| 57 | |
| 58 | @Query("SELECT a FROM Author a JOIN FETCH a.books") |
| 59 | List<Author> findAllWithBooks(); |
| 60 | } |
| 61 | |
| 62 | // Usage - single query |
| 63 | List<Author> authors = authorRepository.findAllWithBooks(); |
| 64 | ``` |
| 65 | |
| 66 | ### Solution 2: @EntityGraph |
| 67 | |
| 68 | ```java |
| 69 | // ✅ GOOD: EntityGraph for declarative fetching |
| 70 | public interface AuthorRepository extends JpaRepository<Author, Long> { |
| 71 | |
| 72 | @EntityGraph(attributePaths = {"books"}) |
| 73 | List<Author> findAll(); |
| 74 | |
| 75 | // Or with named graph |
| 76 | @EntityGraph(value = "Author.withBooks") |
| 77 | List<Author> findAllWithBooks(); |
| 78 | } |
| 79 | |
| 80 | // Define named graph on entity |
| 81 | @Entity |
| 82 | @NamedEntityGraph( |
| 83 | name = "Author.withBooks", |
| 84 | attributeNodes = @NamedAttributeNode("books") |
| 85 | ) |
| 86 | public class Author { |
| 87 | // ... |
| 88 | } |
| 89 | ``` |
| 90 | |
| 91 | ### Solution 3: Batch Fetching |
| 92 | |
| 93 | ```java |
| 94 | // ✅ GOOD: Batch fetching (Hibernate-specific) |
| 95 | @Entity |
| 96 | public class Author { |
| 97 | |
| 98 | @OneToMany(mappedBy = "author") |
| 99 | @BatchSize(size = 25) // Fetch 25 at a time |
| 100 | private List<Book> books; |
| 101 | } |
| 102 | |
| 103 | // Or globally in application.properties |
| 104 | spring.jpa.properties.hibernate.default_batch_fetch_size=25 |
| 105 | ``` |
| 106 | |
| 107 | ### Detecting N+1 |
| 108 | |
| 109 | ```yaml |
| 110 | # Enable SQL logging to detect N+1 |
| 111 | spring: |
| 112 | jpa: |
| 113 | show-sql: true |
| 114 | properties: |
| 115 | hibernate: |
| 116 | format_sql: true |
| 117 | |
| 118 | logging: |
| 119 | level: |
| 120 | org.hibernate.SQL: DEBUG |
| 121 | org.hibernate.type.descriptor.sql.BasicBinder: TRACE |
| 122 | ``` |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## Lazy Loading |
| 127 | |
| 128 | ### FetchType Basics |
| 129 | |
| 130 | ```java |
| 131 | @Entity |
| 132 | public class Order { |
| 133 | |
| 134 | // LAZY: Load only when accessed (default for collections) |
| 135 | @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) |
| 136 | private List<OrderItem> items; |
| 137 | |
| 138 | // EAGER: Always load immediately (default for @ManyToOne, @OneToOne) |
| 139 | @ManyToOne(fetch = FetchType.EAGER) // ⚠️ Usually bad |
| 140 | private Customer customer; |
| 141 | } |
| 142 | ``` |
| 143 | |
| 144 | ### Best Practice: Default to LAZY |
| 145 | |
| 146 | ```java |
| 147 | // ✅ GOOD: Always use LAZY, fetch when needed |
| 148 | @Entity |
| 149 | public class Order { |
| 150 | |
| 151 | @ManyToOne(fetch = FetchType.LAZY) // Override EAGER default |
| 152 | private Customer customer; |
| 153 | |
| 154 | @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) |
| 155 | private List<OrderItem> items; |
| 156 | } |
| 157 | ``` |
| 158 | |
| 159 | ### LazyInitializationException |
| 160 | |
| 161 | ```java |
| 162 | // ❌ BAD: Accessing lazy field outside transaction |
| 163 | @Service |
| 164 | public class OrderService { |
| 165 | |
| 166 | public Order getOrder(Long id) { |
| 167 | return orderRepository.findById(id).orElseThrow(); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | // In controller (no transaction) |
| 172 | Order order = orderService.getOrder(1L); |
| 173 | order.getItems().size(); // 💥 LazyInitializationException! |
| 174 | ``` |
| 175 | |
| 176 | ### Solutions for LazyInitializationException |
| 177 | |
| 178 | **Solution 1: JOIN FETCH in query** |
| 179 | ```java |
| 180 | // ✅ Fetch needed associations in query |
| 181 | @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id") |
| 182 | Optional<Order> findByIdWithItems(@Param("id") Long id); |
| 183 | ``` |
| 184 | |
| 185 | **Solution 2: @Transactional on service method** |
| 186 | ```java |
| 187 | // ✅ Keep transaction open while accessing |
| 188 | @Service |
| 189 | public class OrderService { |
| 190 | |
| 191 | @Transactional(readOnly = true) |
| 192 | public OrderDTO getOrderWithItems(Long id) { |
| 193 | Order order = orderRepository.findById(id).orElseThrow(); |
| 194 | // Access within transaction |
| 195 | int itemCount = order.getItems().size(); |
| 196 | return new OrderDTO(order, itemCount); |
| 197 | } |
| 198 | } |
| 199 | ``` |
| 200 | |
| 201 | **Solution 3: DTO Projection (recommended)** |
| 202 | ```java |
| 203 | // ✅ BEST: Return only what you need |
| 204 | pub |