JPA 和高级关联:完整类指南
本指南提供了关键 JPA 概念的完整类,重点介绍关联、获取优化和投影技术,包括基本示例、场景和常见陷阱。
I. JPA 介绍和关联类型
JPA(Java 持久化 API),通常使用 Hibernate 实现,是 Java 中 对象关系映射(ORM) 的基本工具。它允许透明地管理 Java 对象在关系数据库中的持久化。
什么是关联?
JPA 中的关联定义了实体在领域模型中如何相互关联。它们代表业务对象之间的现实世界关系。
| 关联类型 | 描述 | 实际示例 |
|---|---|---|
@OneToOne | 一个实例关联到恰好一个 | 用户 → 档案 |
@OneToMany | 一个实例关联到多个 | 订单 → 订单行 |
@ManyToOne | 多个实例关联到一个 | 员工 → 部门 |
@ManyToMany | 多个实例相互关联 | 学生 ↔ 课程 |
基本概念
在深入之前,理解这些概念很重要:
- 所有者端:管理数据库中外键(FK)的一方
- 反向端:使用
mappedBy且不管理 FK 的一方 - FetchType:定义何时加载数据(
LAZYvsEAGER) - 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 中的常见错误
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 上下文不一致 | 未同步双方 | 使用实用方法(addLine、removeLine) |
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 | 使用 Set | Set<Course> courses = new HashSet<>() | ❌ List 导致大规模删除和重新插入 |
| 2 | 实用方法 | addCourse()、removeCourse() | ❌ 如果双方未同步则上下文不一致 |
| 3 | FetchType.LAZY | 默认值,不更改 | ❌ EAGER 导致性能问题 |
| 4 | 避免危险的 CascadeType | 仅 PERSIST 和 MERGE | ❌ REMOVE 可能删除共享数据 |
| 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=25E. 陷阱: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();解决方案
- 将
List转换为Set:
@OneToMany(mappedBy = "order")
private Set<OrderLine> lines = new HashSet<>();
@OneToMany(mappedBy = "order")
private Set<Payment> payments = new HashSet<>();- 多个单独查询:
@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 等专家建立的最佳实践。要深入了解,请查阅官方文档和提到的资源。