Zademy

JPA 和高级关联:完整类指南

JPA; Hibernate; Spring Data; ORM; Performance
4257 字

本指南提供了关键 JPA 概念的完整类,重点介绍关联、获取优化和投影技术,包括基本示例、场景和常见陷阱。

I. JPA 介绍和关联类型

JPA(Java 持久化 API),通常使用 Hibernate 实现,是 Java 中 对象关系映射(ORM) 的基本工具。它允许透明地管理 Java 对象在关系数据库中的持久化。

什么是关联?

JPA 中的关联定义了实体在领域模型中如何相互关联。它们代表业务对象之间的现实世界关系。

关联类型描述实际示例
@OneToOne一个实例关联到恰好一个用户 → 档案
@OneToMany一个实例关联到多个订单 → 订单行
@ManyToOne多个实例关联到一个员工 → 部门
@ManyToMany多个实例相互关联学生 ↔ 课程

基本概念

在深入之前,理解这些概念很重要:

  • 所有者端:管理数据库中外键(FK)的一方
  • 反向端:使用 mappedBy 且不管理 FK 的一方
  • FetchType:定义何时加载数据(LAZY vs EAGER
  • CascadeType:定义哪些操作传播到相关实体

II. 一对一关系(@OneToOne

@OneToOne 关系用于实体 A 的一个实例与实体 B 的一个实例关联时。

使用场景

  • 数据分离:很少访问的字段在单独的表中
  • 可扩展性:具有高写入操作的主实体
  • 安全性:敏感数据在具有不同权限的单独表中

默认行为

⚠️ 默认情况下,@OneToOne 使用 FetchType.EAGER,这可能导致性能问题。

基本示例:单向

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "profile_id", referencedColumnName = "id")
    private UserProfile profile;

    // Getters and setters
}

@Entity
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String bio;
    private String avatarUrl;

    // Getters and setters
}

高级示例:使用 @MapsId 的双向

@MapsId 技术是双向 @OneToOne最佳实践,因为:

  • 在两个表之间共享主键
  • 在反向端启用真正的懒加载
  • 减少存储(无额外 FK)
@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;

    @OneToOne(mappedBy = "customer", cascade = CascadeType.ALL,
              fetch = FetchType.LAZY, orphanRemoval = true)
    private CustomerDetails details;

    // 维护同步的实用方法
    public void setDetails(CustomerDetails details) {
        if (details == null) {
            if (this.details != null) {
                this.details.setCustomer(null);
            }
        } else {
            details.setCustomer(this);
        }
        this.details = details;
    }
}

@Entity
public class CustomerDetails {
    @Id
    private Long id; // 与 Customer 相同的 PK

    private String address;
    private String phone;
    private LocalDate birthDate;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId // 将 Customer 的 PK 映射为 FK 和 PK
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // Getters and setters
}

生成的 SQL

CREATE TABLE customer (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    email VARCHAR(255)
);

CREATE TABLE customer_details (
    customer_id BIGINT PRIMARY KEY,  -- PK 和 FK 同时
    address VARCHAR(255),
    phone VARCHAR(255),
    birth_date DATE,
    FOREIGN KEY (customer_id) REFERENCES customer(id)
);

@OneToOne 中的常见错误

错误原因解决方案
N+1 与 EAGER默认 FetchType.EAGER使用 FetchType.LAZY
懒加载在反向端不起作用Hibernate 无法创建代理使用 @MapsId 或字节码增强
孤儿数据父级删除时详情未删除使用 orphanRemoval = true

III. 一对多(@OneToMany)和多对一(@ManyToOne)关系

这些关联是 固有双重的:如果实体有 @ManyToOne,从另一侧查看的关系是 @OneToMany

A. @ManyToOne - 外键端

此关联定义在 包含外键(FK) 的实体端。多个子实体关联到一个父实体。

默认行为

⚠️ 默认情况下,@ManyToOne 使用 FetchType.EAGER。始终将其更改为 LAZY

基本示例

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY) // 始终 LAZY!
    @JoinColumn(name = "department_id", nullable = false)
    private Department department;

    // Getters and setters
}

B. @OneToMany - 集合端

此关联定义在 拥有相关实体集合 的实体端。

默认行为

✅ 默认情况下,@OneToMany 使用 FetchType.LAZY,这是正确的。

基本示例

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL,
               orphanRemoval = true)
    private List<Employee> employees = new ArrayList<>();

    // Getters and setters
}

C. 双向性和所有权

实现双向关联时,JPA 要求一侧是所有者,另一侧是反向

黄金法则

🔑 在双向 @OneToMany / @ManyToOne 关联中,"多" 端(@ManyToOne)必须始终是所有者

完整示例:订单和订单行

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime orderDate;
    private String status;

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

    // ✅ 维护双向同步的实用方法
    public void addLine(OrderLine line) {
        lines.add(line);
        line.setOrder(this);
    }

    public void removeLine(OrderLine line) {
        lines.remove(line);
        line.setOrder(null);
    }
}

@Entity
public class OrderLine {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String productName;
    private Integer quantity;
    private BigDecimal price;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order; // 此端管理 FK

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof OrderLine)) return false;
        OrderLine that = (OrderLine) o;
        return id != null && id.equals(that.getId());
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

生成的 SQL

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_date DATETIME,
    status VARCHAR(50)
);

CREATE TABLE order_line (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(255),
    quantity INT,
    price DECIMAL(10,2),
    order_id BIGINT NOT NULL,  -- FK 由 @ManyToOne 管理
    FOREIGN KEY (order_id) REFERENCES orders(id)
);

性能陷阱:为什么多端必须是所有者?

场景SQL 查询解释
@ManyToOne 作为所有者N + 1每个 OrderLine 1 个 INSERT
@OneToMany 作为所有者2N + 1每个 OrderLine 1 个 INSERT + 1 个 FK UPDATE
// ❌ 错误:`@OneToMany` 作为所有者(无 mappedBy)
@OneToMany
@JoinColumn(name = "order_id") // 这使 Order 成为所有者
private List<OrderLine> lines;

// 结果:INSERT order_line + UPDATE order_line SET order_id = ?
// ✅ 好:`@ManyToOne` 作为所有者
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;

// 结果:INSERT order_line(包含 order_id)

@OneToMany / @ManyToOne 中的常见错误

错误原因解决方案
上下文不一致未同步双方使用实用方法(addLineremoveLine
LazyInitializationException在事务外访问集合使用 JOIN FETCH@Transactional
集合中的重复项未实现 equals/hashCode基于 ID 或业务键实现
性能下降@ManyToOne 使用 EAGER始终使用 FetchType.LAZY

IV. 多对多关系(@ManyToMany

@ManyToMany 关联非常常见,但存在必须仔细避免的几个 陷阱

基本示例:双向

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>(); // ✅ 使用 Set,而不是 List

    // 实用方法
    public void addCourse(Course course) {
        courses.add(course);
        course.getStudents().add(this);
    }

    public void removeCourse(Course course) {
        courses.remove(course);
        course.getStudents().remove(this);
    }
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Course)) return false;
        Course course = (Course) o;
        return id != null && id.equals(course.getId());
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

生成的 SQL

CREATE TABLE student (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255)
);

CREATE TABLE course (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255)
);

CREATE TABLE student_course (
    student_id BIGINT NOT NULL,
    course_id BIGINT NOT NULL,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);

最佳实践和常见错误

#实践实现潜在错误
1使用 SetSet<Course> courses = new HashSet<>()List 导致大规模删除和重新插入
2实用方法addCourse()removeCourse()❌ 如果双方未同步则上下文不一致
3FetchType.LAZY默认值,不更改EAGER 导致性能问题
4避免危险的 CascadeTypePERSISTMERGEREMOVE 可能删除共享数据
5实现 equals/hashCode基于 ID 或业务键❌ 重复项和异常行为

为什么 @ManyToMany 中不使用 List

// ❌ 错误:使用 List
@ManyToMany
private List<Course> courses = new ArrayList<>();

// 删除一个课程时,Hibernate 执行:
// DELETE FROM student_course WHERE student_id = ?  -- 全部!
// INSERT INTO student_course VALUES (?, ?)         -- 重新插入剩余
// INSERT INTO student_course VALUES (?, ?)
// ...
// ✅ 好:使用 Set
@ManyToMany
private Set<Course> courses = new HashSet<>();

// 删除一个课程时,Hibernate 执行:
// DELETE FROM student_course WHERE student_id = ? AND course_id = ?  -- 仅一个

高级模式:链接实体

当您需要关系中的 额外属性(注册日期、成绩等)时,使用链接实体:

@Entity
public class Enrollment {
    @EmbeddedId
    private EnrollmentId id;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("studentId")
    @JoinColumn(name = "student_id")
    private Student student;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("courseId")
    @JoinColumn(name = "course_id")
    private Course course;

    private LocalDate enrollmentDate;
    private Double grade;

    // 构造函数、getter、setter
}

@Embeddable
public class EnrollmentId implements Serializable {
    private Long studentId;
    private Long courseId;

    // equals, hashCode
}

V. 高效数据加载:N+1 问题和获取策略

A. N+1 SELECT 问题

N+1 问题发生在:

  • 1 个查询 执行以获取主实体
  • N 个额外查询 加载每个实体的关联

问题示例

// 导致 N+1 的代码
List<Order> orders = orderRepository.findAll(); // 1 个查询

for (Order order : orders) {
    // 每次访问 lines 都会触发额外查询
    System.out.println(order.getLines().size()); // N 个查询
}
-- 查询 1:获取订单
SELECT * FROM orders;

-- 查询 N:每个订单一个
SELECT * FROM order_line WHERE order_id = 1;
SELECT * FROM order_line WHERE order_id = 2;
SELECT * FROM order_line WHERE order_id = 3;
-- ... N 次

B. 解决方案 1:JOIN FETCH(JPQL)

JOIN FETCH 子句告诉 Hibernate 立即初始化哪些关联。

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.lines WHERE o.status = :status")
    List<Order> findByStatusWithLines(@Param("status") String status);

    @Query("SELECT DISTINCT o FROM Order o " +
           "JOIN FETCH o.lines l " +
           "JOIN FETCH o.customer " +
           "WHERE o.orderDate >= :date")
    List<Order> findRecentOrdersWithDetails(@Param("date") LocalDateTime date);
}
-- 带 JOIN 的单个查询
SELECT DISTINCT o.*, l.*, c.*
FROM orders o
INNER JOIN order_line l ON o.id = l.order_id
INNER JOIN customer c ON o.customer_id = c.id
WHERE o.order_date >= ?

C. 解决方案 2:实体图

实体图 允许声明式定义要加载的关联。

使用 @NamedEntityGraph 定义

@Entity
@NamedEntityGraph(
    name = "Order.withLinesAndCustomer",
    attributeNodes = {
        @NamedAttributeNode("lines"),
        @NamedAttributeNode("customer")
    }
)
public class Order {
    // ...
}

在仓库中的使用

public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(value = "Order.withLinesAndCustomer")
    List<Order> findByStatus(String status);

    // 或定义内联
    @EntityGraph(attributePaths = {"lines", "customer"})
    Optional<Order> findById(Long id);
}

编程使用

@Service
@Transactional(readOnly = true)
public class OrderService {

    @PersistenceContext
    private EntityManager em;

    public List<Order> findOrdersWithDetails() {
        EntityGraph<Order> graph = em.createEntityGraph(Order.class);
        graph.addAttributeNodes("lines");
        graph.addSubgraph("customer").addAttributeNodes("details");

        return em.createQuery("SELECT o FROM Order o", Order.class)
                 .setHint("jakarta.persistence.loadgraph", graph)
                 .getResultList();
    }
}

D. 解决方案 3:批处理获取

批处理获取 将 N+1 减少到 N/batchSize + 1 个查询。

@Entity
public class Order {

    @OneToMany(mappedBy = "order")
    @BatchSize(size = 25) // 每个查询最多加载 25 个集合
    private List<OrderLine> lines;
}
-- 而不是 N 个单独查询:
SELECT * FROM order_line WHERE order_id IN (1, 2, 3, ..., 25);
SELECT * FROM order_line WHERE order_id IN (26, 27, 28, ..., 50);
-- 等等。

全局配置

# application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size=25

E. 陷阱:MultipleBagFetchException

错误cannot simultaneously fetch multiple bags

尝试 JOIN FETCH 多个 List 集合(包)时发生。

// ❌ 这会失败
@Query("SELECT o FROM Order o JOIN FETCH o.lines JOIN FETCH o.payments")
List<Order> findWithLinesAndPayments();

解决方案

  1. List 转换为 Set
@OneToMany(mappedBy = "order")
private Set<OrderLine> lines = new HashSet<>();

@OneToMany(mappedBy = "order")
private Set<Payment> payments = new HashSet<>();
  1. 多个单独查询
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.lines")
List<Order> findWithLines();

@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.payments WHERE o IN :orders")
List<Order> fetchPayments(@Param("orders") List<Order> orders);

F. 何时不使用 JOIN FETCH

⚠️ 有时,N+1 大量 JOIN 更好。

如果带有多个 JOIN 的查询生成巨大的 笛卡尔积

54 个订单 × 54 个行 × 54 个支付 = 157,464 行

执行 3 个简单查询更高效:

54 + 54 + 54 = 162 总行数

经验法则:对 1-2 个关联使用 JOIN FETCH。对于更多关联,考虑批处理获取或单独查询。


VI. 投影:优化数据检索

投影 允许仅检索必要数据,提高性能并减少内存使用。

A. 基于接口的投影

最简单的形式。Spring Data JPA 自动创建代理。

// 定义投影
public interface OrderSummary {
    Long getId();
    LocalDateTime getOrderDate();
    String getStatus();

    // 使用 SpEL 的计算属性
    @Value("#{target.lines.size()}")
    int getLineCount();
}

// 在仓库中使用
public interface OrderRepository extends JpaRepository<Order, Long> {

    List<OrderSummary> findByStatus(String status);

    @Query("SELECT o.id as id, o.orderDate as orderDate, o.status as status " +
           "FROM Order o WHERE o.customer.id = :customerId")
    List<OrderSummary> findSummariesByCustomer(@Param("customerId") Long customerId);
}

限制

  • 无法自定义 equals() / hashCode()
  • 对于复杂查询效率低于 DTO

B. 基于类的投影(DTO)

对复杂查询有更多控制和更好性能。

使用记录(Java 16+)

public record OrderDTO(
    Long id,
    LocalDateTime orderDate,
    String status,
    String customerName,
    BigDecimal totalAmount
) {}

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("""
        SELECT new com.example.dto.OrderDTO(
            o.id,
            o.orderDate,
            o.status,
            c.name,
            SUM(l.price * l.quantity)
        )
        FROM Order o
        JOIN o.customer c
        JOIN o.lines l
        WHERE o.status = :status
        GROUP BY o.id, o.orderDate, o.status, c.name
        """)
    List<OrderDTO> findOrderSummaries(@Param("status") String status);
}

使用传统 POJO

public class OrderDetailDTO {
    private Long orderId;
    private String customerName;
    private List<LineDTO> lines;

    public OrderDetailDTO(Long orderId, String customerName) {
        this.orderId = orderId;
        this.customerName = customerName;
        this.lines = new ArrayList<>();
    }

    // Getters, setters
}

public class LineDTO {
    private String productName;
    private Integer quantity;
    private BigDecimal price;

    public LineDTO(String productName, Integer quantity, BigDecimal price) {
        this.productName = productName;
        this.quantity = quantity;
        this.price = price;
    }
}

C. 动态投影

允许在运行时选择投影类型。

public interface OrderRepository extends JpaRepository<Order, Long> {

    // 接受任何投影类型的通用方法
    <T> List<T> findByStatus(String status, Class<T> type);

    <T> Optional<T> findById(Long id, Class<T> type);
}

// 使用
List<OrderSummary> summaries = orderRepository.findByStatus("PENDING", OrderSummary.class);
List<OrderDTO> dtos = orderRepository.findByStatus("PENDING", OrderDTO.class);
List<Order> entities = orderRepository.findByStatus("PENDING", Order.class);

D. 元组投影

用于即席查询而不创建 DTO。

@Query("SELECT o.id, o.status, COUNT(l) FROM Order o LEFT JOIN o.lines l GROUP BY o.id, o.status")
List<Object[]> findOrderStats();

// 或使用 Tuple
@Query("SELECT o.id as id, o.status as status, COUNT(l) as lineCount " +
       "FROM Order o LEFT JOIN o.lines l GROUP BY o.id, o.status")
List<Tuple> findOrderStatsTuple();

// 使用
List<Tuple> stats = orderRepository.findOrderStatsTuple();
for (Tuple t : stats) {
    Long id = t.get("id", Long.class);
    String status = t.get("status", String.class);
    Long count = t.get("lineCount", Long.class);
}

投影比较

类型优点缺点推荐使用
接口简单,自动无 equals/hashCode,反射简单查询
DTO/记录完全控制,高效更多代码复杂查询
动态灵活类型安全性较低具有多个视图的 API
元组无需额外类可读性较差即席查询

VII. 原生查询

原生查询允许直接执行 SQL,在 JPQL 不够用时是必要的。

A. JPQL vs 原生查询

功能JPQL原生查询
抽象数据库无关数据库特定
复杂性复杂查询受限可用完整 SQL
可移植性
数据库函数受限全部可用

B. 基本语法

public interface OrderRepository extends JpaRepository<Order, Long> {

    // 简单原生查询
    @Query(value = "SELECT * FROM orders WHERE status = ?1", nativeQuery = true)
    List<Order> findByStatusNative(String status);

    // 使用 @NativeQuery(Spring Data 3.x)
    @NativeQuery("SELECT * FROM orders WHERE YEAR(order_date) = :year")
    List<Order> findByYear(@Param("year") int year);

    // 带分页
    @Query(
        value = "SELECT * FROM orders WHERE status = :status",
        countQuery = "SELECT COUNT(*) FROM orders WHERE status = :status",
        nativeQuery = true
    )
    Page<Order> findByStatusPaged(@Param("status") String status, Pageable pageable);
}

C. 原生查询的投影

// 投影到接口
public interface OrderNativeSummary {
    Long getId();
    String getStatus();
    Integer getLineCount();
}

@Query(value = """
    SELECT o.id, o.status, COUNT(l.id) as lineCount
    FROM orders o
    LEFT JOIN order_line l ON o.id = l.order_id
    GROUP BY o.id, o.status
    """, nativeQuery = true)
List<OrderNativeSummary> findNativeSummaries();

// 投影到 Map
@NativeQuery("SELECT * FROM orders WHERE id = :id")
Map<String, Object> findRawById(@Param("id") Long id);

D. 原生查询的分层 DTO

对于复杂结构,使用带有 ResultTransformer自定义仓库

// 分层 DTO
public class OrderWithLinesDTO {
    private Long id;
    private String status;
    private List<LineDTO> lines = new ArrayList<>();
}

// 自定义仓库实现
@Repository
public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {

    @PersistenceContext
    private EntityManager em;

    @Override
    @SuppressWarnings("unchecked")
    public List<OrderWithLinesDTO> findOrdersWithLinesNative() {
        String sql = """
            SELECT o.id as orderId, o.status,
                   l.id as lineId, l.product_name, l.quantity, l.price
            FROM orders o
            LEFT JOIN order_line l ON o.id = l.order_id
            ORDER BY o.id
            """;

        List<Object[]> results = em.createNativeQuery(sql).getResultList();

        Map<Long, OrderWithLinesDTO> orderMap = new LinkedHashMap<>();

        for (Object[] row : results) {
            Long orderId = ((Number) row[0]).longValue();

            OrderWithLinesDTO order = orderMap.computeIfAbsent(orderId, id -> {
                OrderWithLinesDTO dto = new OrderWithLinesDTO();
                dto.setId(id);
                dto.setStatus((String) row[1]);
                return dto;
            });

            if (row[2] != null) { // 如果有行
                LineDTO line = new LineDTO(
                    (String) row[3],
                    ((Number) row[4]).intValue(),
                    (BigDecimal) row[5]
                );
                order.getLines().add(line);
            }
        }

        return new ArrayList<>(orderMap.values());
    }
}

E. 何时使用原生查询

场景推荐
数据库特定函数(JSONB、数组)✅ 原生
窗口函数(ROW_NUMBER、RANK)✅ 原生
CTE(WITH 子句)✅ 原生
简单 CRUD 查询❌ 使用 JPQL 或查询方法
数据库间可移植性❌ 使用 JPQL

VIII. 自定义仓库

当标准选项不够时,实现自定义仓库。

结构

// 1. 带自定义方法的接口
public interface OrderRepositoryCustom {
    List<Order> findOrdersDynamic(OrderSearchCriteria criteria);
    List<OrderWithLinesDTO> findOrdersWithLinesNative();
}

// 2. 实现(名称必须以 "Impl" 结尾)
@Repository
public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {

    @PersistenceContext
    private EntityManager em;

    @Override
    public List<Order> findOrdersDynamic(OrderSearchCriteria criteria) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Order> query = cb.createQuery(Order.class);
        Root<Order> order = query.from(Order.class);

        List<Predicate> predicates = new ArrayList<>();

        if (criteria.getStatus() != null) {
            predicates.add(cb.equal(order.get("status"), criteria.getStatus()));
        }

        if (criteria.getFromDate() != null) {
            predicates.add(cb.greaterThanOrEqualTo(
                order.get("orderDate"), criteria.getFromDate()));
        }

        if (criteria.getCustomerId() != null) {
            predicates.add(cb.equal(
                order.get("customer").get("id"), criteria.getCustomerId()));
        }

        // 动态 JOIN FETCH
        if (criteria.isFetchLines()) {
            order.fetch("lines", JoinType.LEFT);
        }

        query.where(predicates.toArray(new Predicate[0]));
        query.distinct(true);

        return em.createQuery(query).getResultList();
    }
}

// 3. 主仓库扩展两者
public interface OrderRepository extends
        JpaRepository<Order, Long>,
        OrderRepositoryCustom {
    // 标准查询方法在这里
}

IX. 最佳实践总结

关联

实践描述
✅ 始终 FetchType.LAZY除了非常特殊的情况
@ManyToOne 作为所有者在双向关系中
Set 用于 @ManyToMany避免大规模删除
✅ 实用方法维护双向同步
@MapsId 用于 @OneToOne更好的性能和懒加载
@ManyToMany 中的 CascadeType.REMOVE可能删除共享数据
@ManyToMany 中的 List导致大规模删除和重新插入

获取

实践描述
JOIN FETCH 用于 1-2 个关联最常见的 N+1 解决方案
✅ 实体图用于声明式情况比 JPQL 更易读
✅ 批处理获取用于多个集合避免 MultipleBagFetchException
❌ 带 List 的多个 JOIN FETCH导致 MultipleBagFetchException
❌ 生成巨大笛卡尔积的大量 JOIN有时 N+1 更好

投影

实践描述
✅ 读取时使用投影比完整实体更好的性能
✅ DTO 的记录不可变且简洁
✅ 简单情况的接口投影代码更少
✅ 复杂情况的自定义仓库完全控制

X. 代码审查清单

审查 JPA 代码时使用此列表:

  • 所有 @ManyToOne@OneToOne 关联是否都有 FetchType.LAZY
  • @ManyToMany 集合是否使用 Set 而不是 List
  • 双向关系中 @ManyToOne 端是否是所有者?
  • 是否有实用方法来同步双向关联?
  • 实体中是否正确实现了 equals()hashCode()
  • 加载集合的查询是否使用 JOIN FETCH 或实体图?
  • 只读查询是否使用投影?
  • @ManyToMany 中是否避免了 CascadeType.REMOVE
  • 带多个 JOIN 的查询是否不生成巨大的笛卡尔积?

本指南基于官方 Hibernate ORM 文档、Spring Data JPA 以及 Vlad Mihalcea 和 Thorben Janssen 等专家建立的最佳实践。要深入了解,请查阅官方文档和提到的资源。