关于 jpa-patterns
jpa-patterns 是面向 Spring 应用中 JPA 和 Hibernate 的实用参考技能。它帮助开发者排查查询、加载、事务和并发问题,并介绍抓取连接、投影、分页和乐观锁等模式。
主要功能
- 01诊断 N+1 查询问题
- 02说明延迟加载与抓取策略
- 03介绍事务管理常见陷阱
- 04指导查询优化与分页
- 05展示乐观锁和现代 Hibernate 功能
适用场景
- 01减少多余的数据库查询
- 02解决 LazyInitializationException
- 03优化缓慢的只读查询
- 04处理实体并发更新
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ piomin/ claude-ai-spring-boot --skill jpa-patterns安装后在 Claude Code 中输入/jpa-patterns确认已加载。
详细介绍
JPA 模式技能
Spring 应用中 JPA/Hibernate 的最佳实践和常见陷阱。
适用场景
- 用户提到“N+1 问题”/“查询太多”
- LazyInitializationException 错误
- 关于抓取策略(EAGER 与 LAZY)的问题
- 事务管理问题
- 实体关系设计
- 查询优化
快速参考:常见问题
| 问题 | 症状 | 解决方案 |
|---|---|---|
| N+1 查询 | 出现许多 SELECT 语句 | JOIN FETCH、@EntityGraph |
| LazyInitializationException | 事务外出现错误 | Open Session in View、DTO 投影、JOIN FETCH |
| 查询缓慢 | 性能问题 | 分页、投影、索引 |
| 脏检查开销 | 更新缓慢 | 只读事务、DTO |
| 更新丢失 | 并发修改 | 乐观锁(@Version) |
N+1 问题
JPA 性能的头号杀手
问题
// ❌ BAD: N+1 queries
@Entity
public class Author {
@Id private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}
// This innocent code...
List<Author> authors = authorRepository.findAll(); // 1 query
for (Author author : authors) {
System.out.println(author.getBooks().size()); // N queries!
}
// Result: 1 + N queries (if 100 authors = 101 queries)
解决方案 1:JOIN FETCH(JPQL)
// ✅ GOOD: Single query with JOIN FETCH
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
}
// Usage - single query
List<Author> authors = authorRepository.findAllWithBooks();
解决方案 2:@EntityGraph
// ✅ GOOD: EntityGraph for declarative fetching
public interface AuthorRepository extends JpaRepository<Author, Long> {
@EntityGraph(attributePaths = {"books"})
List<Author> findAll();
// Or with named graph
@EntityGraph(value = "Author.withBooks")
List<Author> findAllWithBooks();
}
// Define named graph on entity
@Entity
@NamedEntityGraph(
name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
// ...
}
解决方案 3:批量抓取
// ✅ GOOD: Batch fetching (Hibernate-specific)
@Entity
public class Author {
@OneToMany(mappedBy = "author")
@BatchSize(size = 25) // Fetch 25 at a time
private List<Book> books;
}
// Or globally in application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size=25
检测 N+1
# Enable SQL logging to detect N+1
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
延迟加载
FetchType 基础知识
@Entity
public class Order {
// LAZY: Load only when accessed (default for collections)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
// EAGER: Always load immediately (default for @ManyToOne, @OneToOne)
@ManyToOne(fetch = FetchType.EAGER) // ⚠️ Usually bad
private Customer customer;
}
最佳实践:默认使用 LAZY
// ✅ GOOD: Always use LAZY, fetch when needed
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // Override EAGER default
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
LazyInitializationException
// ❌ BAD: Accessing lazy field outside transaction
@Service
public class OrderService {
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
}
// In controller (no transaction)
Order order = orderService.getOrder(1L);
order.getItems().size(); // 💥 LazyInitializationException!
LazyInitializationException 的解决方案
解决方案 1:在查询中使用 JOIN FETCH
// ✅ Fetch needed associations in query
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
解决方案 2:在服务方法上使用 @Transactional
// ✅ Keep transaction open while accessing
@Service
public class OrderService {
@Transactional(readOnly = true)
public OrderDTO getOrderWithItems(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
// Access within transaction - lazy collection loads here
int itemCount = order.getItems().size();
return new OrderDTO(order, itemCount);
}
}
解决方案 3:DTO 投影(最适合只读场景)
// ✅ Avoid lazy loading entirely - query only what you need
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT new pl.piomin.services.dto.OrderSummary(o.id, o.status, SIZE(o.items)) " +
"FROM Order o WHERE o.id = :id")
Optional<OrderSummary> findSummaryById(@Param("id") Long id);
}
事务管理
只读优化
@Service
@Transactional(readOnly = true) // Default for all methods
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo;
}
public List<Product> findAll() {
return repo.findAll(); // Read-only: skips dirty checking
}
@Transactional // Override: read-write for mutations
public Product create(ProductRequest request) {
var product = new Product();
product.setName(request.name());
return repo.save(product);
}
}
自调用陷阱
// ❌ BAD: @Transactional ignored on self-invocation
@Service
public class OrderService {
public void processOrders(List<Long> ids) {
for (Long id : ids) {
processOrder(id); // Calls via 'this', not proxy - no transaction!
}
}
@Transactional
public void processOrder(Long id) {
// This @Transactional is ignored when called from processOrders()
}
}
// ✅ GOOD: Extract to separate bean
@Service
public class OrderService {
private final OrderProcessor processor;
public OrderService(OrderProcessor processor) {
this.processor = processor;
}
public void processOrders(List<Long> ids) {
for (Long id : ids) {
processor.processOrder(id); // Calls via proxy
}
}
}
@Service
public class OrderProcessor {
@Transactional
public void processOrder(Long id) {
// Transaction works correctly
}
}
乐观锁
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
@Version
private Long version; // Hibernate checks this on update
// getters/setters
}
// Handling optimistic lock failure
@Service
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo;
}
@Transactional
public Product updatePrice(Long id, BigDecimal newPrice) {
Product product = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Product not found"));
product.setPrice(newPrice);
try {
return repo.save(product);
} catch (OptimisticLockingFailureException e) {
throw new ConflictException("Product was modified by another user");
}
}
}
Hibernate 现代特性
Jakarta Persistence
// Always use jakarta.persistence.* (not javax.persistence.*)
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
UUID 生成
@Entity
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.UUID) // Native UUID generation
private UUID id;
private String name;
// getters/setters
}
软删除
// Built-in soft delete support
@Entity
@SoftDelete // Adds 'deleted' boolean column, filters automatically
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getters/setters
}
// Custom column name
@Entity
@SoftDelete(columnName = "is_active", converter = NumericBooleanConverter.class)
public class Product {
// ...
}
在查询中将 Record 用作 DTO
// Records work natively in JPQL constructor expressions
public record ProductSummary(Long id, String name, BigDecimal price) {}
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT new pl.piomin.services.dto.ProductSummary(p.id, p.name, p.price) " +
"FROM Product p WHERE p.price > :minPrice")
List<ProductSummary> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}
查询最佳实践
分页
// ✅ Always paginate unbounded queries
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByActiveTrue(Pageable pageable);
Slice<User> findByRole(String role, Pageable pageable); // No COUNT query
}
// Usage
Page<User> page = userRepository.findByActiveTrue(
PageRequest.of(0, 20, Sort.by("createdAt").descending())
);
Spring Data 投影
// Interface-based projection - fetches only declared fields
public interface UserNameOnly {
String getFirstName();
String getLastName();
}
public interface UserRepository extends JpaRepository<User, Long> {
List<UserNameOnly> findAllByActiveTrue();
}
快速决策指南
| 情况 | 解决方案 |
|---|---|
| 循环中出现 N+1 | JOIN FETCH 或 @EntityGraph |
| LazyInitializationException | 在查询中使用 JOIN FETCH,或在服务上使用 @Transactional |
| 只读查询缓慢 | @Transactional(readOnly = true) |
| 并发更新 | @Version + 乐观锁 |
| 结果集无上限 | 使用 PageRequest 分页 |
| 只需要少量列 | DTO 投影或接口投影 |
| 需要软删除 | @SoftDelete |
| UUID 主键 | @GeneratedValue(strategy = GenerationType.UUID) |
评论
支持 Markdown