mcpField
J
jpa-patterns

jpa-patterns

github.com/piomin开发1.4k71 次安装1 天前更新
代码仓库

关于 jpa-patterns

jpa-patterns 是面向 Spring 应用中 JPA 和 Hibernate 的实用参考技能。它帮助开发者排查查询、加载、事务和并发问题,并介绍抓取连接、投影、分页和乐观锁等模式。

主要功能

  1. 01诊断 N+1 查询问题
  2. 02说明延迟加载与抓取策略
  3. 03介绍事务管理常见陷阱
  4. 04指导查询优化与分页
  5. 05展示乐观锁和现代 Hibernate 功能

适用场景

  1. 01减少多余的数据库查询
  2. 02解决 LazyInitializationException
  3. 03优化缓慢的只读查询
  4. 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+1JOIN FETCH 或 @EntityGraph
LazyInitializationException在查询中使用 JOIN FETCH,或在服务上使用 @Transactional
只读查询缓慢@Transactional(readOnly = true)
并发更新@Version + 乐观锁
结果集无上限使用 PageRequest 分页
只需要少量列DTO 投影或接口投影
需要软删除@SoftDelete
UUID 主键@GeneratedValue(strategy = GenerationType.UUID)

评论

支持 Markdown