mcpField
S
spring-data-jpa

spring-data-jpa

github.com/rrezartprebreza开发334128 次安装3 天前更新
代码仓库

关于 spring-data-jpa

面向 Spring Data JPA 开发者的持久化实践指南,涉及实体建模、查询、分页和批量写入。重点包括避免 N+1 查询、深分页及实体映射中的常见问题。

主要功能

  1. 01实体字段与关系映射规范
  2. 02JOIN FETCH、EntityGraph 与投影
  3. 03派生查询和 JPQL 模式
  4. 04分页与 Keyset 深分页
  5. 05Hibernate 批量插入配置

适用场景

  1. 01设计安全且一致的 JPA 实体
  2. 02排查并减少 N+1 查询
  3. 03为列表接口选择分页策略
  4. 04配置批量写入并检查实体关系

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill spring-data-jpa

安装后在 Claude Code 中输入/spring-data-jpa确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
examples/
bad-entity.java
good-entity.java

详细介绍

Spring Data JPA

实体约定

@Entity
@Table(name = "orders")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requires no-arg, hide from callers
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(updatable = false, nullable = false)
    private UUID id;

    @Column(nullable = false)
    private String customerEmail;

    @Enumerated(EnumType.STRING) // always STRING, never ORDINAL
    @Column(nullable = false)
    private OrderStatus status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    @Column(updatable = false)
    private Instant createdAt;

    @UpdateTimestamp
    private Instant updatedAt;

    // Static factory, not public constructor
    public static Order create(String customerEmail) {
        Order order = new Order();
        order.customerEmail = customerEmail;
        order.status = OrderStatus.PENDING;
        return order;
    }

    // Behavior on entity, not in service
    public void addItem(Product product, int quantity) {
        items.add(OrderItem.create(this, product, quantity));
    }
}

规则

  • 始终使用 @Enumerated(EnumType.STRING)——枚举重排时,ORDINAL 会出问题
  • ID 使用 GenerationType.UUID——绝不公开自增整数
  • 使用 @NoArgsConstructor(access = PROTECTED)——JPA 必需,并对应用代码隐藏
  • 使用 Lombok 的 @Getter——实体上不使用 @Setter(改用行为方法)
  • 内联初始化集合(= new ArrayList<>())——绝不为 null

防止 N+1

识别: 查询订单一次,再为每个订单项查询一次,合计 1 + N 次查询,即 N+1。

使用 JOIN FETCH 修复:

@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") UUID id);

// For lists — use @EntityGraph to avoid duplicates
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByStatus(OrderStatus status);

只读视图使用 Projections 修复:

// Interface projection — no entity loaded
public interface OrderSummary {
    UUID getId();
    String getCustomerEmail();
    OrderStatus getStatus();
    Instant getCreatedAt();
}

List<OrderSummary> findByStatus(OrderStatus status); // fast, no lazy loading issues

查询模式

public interface OrderRepository extends JpaRepository<Order, UUID> {

    // Derived query — simple conditions
    List<Order> findByStatusAndCustomerEmail(OrderStatus status, String email);

    // JPQL — for joins and complex conditions
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
    List<Order> findActiveOrdersWithItems(@Param("status") OrderStatus status);

    // Native SQL — only when JPQL can't do it
    @Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
           nativeQuery = true)
    List<Order> findRecentOrders();

    // Exists check — faster than findById + isPresent
    boolean existsByCustomerEmailAndStatus(String email, OrderStatus status);

    // Projection
    List<OrderSummary> findByCustomerEmail(String email);
}

分页

// Always use Pageable for list endpoints
Page<Order> findByStatus(OrderStatus status, Pageable pageable);

// In service
Page<Order> orders = orderRepository.findByStatus(status, PageRequest.of(page, size, Sort.by("createdAt").descending()));

双向关系

// Parent side (Order)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();

// Child side (OrderItem) — owns the FK
@ManyToOne(fetch = FetchType.LAZY) // LAZY always on @ManyToOne
@JoinColumn(name = "order_id", nullable = false)
private Order order;

// Helper on parent to keep both sides in sync
public void addItem(OrderItem item) {
    items.add(item);
    item.setOrder(this);
}

深度分页——使用 Keyset 而非 OFFSET

OFFSET 分页会扫描并丢弃所有跳过的行。第 5,000 页中,数据库要读取 100,000 行才能 返回 20 行。对于大型数据集或无限滚动的数据集,请按上次看到的键进行分页(即“seek”方法):

// ❌ Slow on deep pages — OFFSET grows linearly
Page<Order> findByStatus(OrderStatus status, Pageable pageable);

// ✅ Keyset — constant time regardless of depth. Pass the last row's createdAt + id.
@Query("""
    SELECT o FROM Order o
    WHERE o.status = :status
      AND (o.createdAt < :lastCreatedAt
           OR (o.createdAt = :lastCreatedAt AND o.id < :lastId))
    ORDER BY o.createdAt DESC, o.id DESC
    """)
List<Order> findNextPage(OrderStatus status, Instant lastCreatedAt, UUID lastId, Limit limit);

(createdAt, id) 元组用于打破平局,确保时间戳重复时游标仍然稳定。索引为 (status, created_at DESC, id DESC)。

批量插入

逐行保存列表会产生 N 次往返。启用 JDBC 批处理,让 Hibernate 对插入进行分组:

spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 50
        order_inserts: true
        order_updates: true

注意:GenerationType.IDENTITY 会悄悄禁用插入批处理(Hibernate 需要逐行获取生成的键)。GenerationType.UUID 或池化序列可以保留批处理——这是优先使用 UUID 的另一个理由。

注意事项

  • Agent 使用 FetchType.EAGER——@ManyToOne 和 @ManyToMany 始终使用 LAZY
  • Agent 使用 @Enumerated(EnumType.ORDINAL)——始终使用 STRING
  • Agent 使用 Long ID——使用 UUID
  • Agent 对列表端点调用 findAll()——始终使用 Pageable
  • Agent 在超大表上使用 OFFSET 分页——深度分页时改用 keyset
  • Agent 为实体添加 setter——改用行为方法
  • Agent 在 @OneToMany 上忘记设置 orphanRemoval = true——子记录会成为孤儿
  • Agent 没意识到自己写出了 N+1——检查循环中是否访问了 items
  • Agent 使用 GenerationType.IDENTITY 批量插入——批处理会悄悄关闭;改用 UUID/序列

评论

支持 Markdown