1. 什么是 JPA 1 + N 问题
在使用 Java Persistence API (JPA) 进行数据库查询时,1 + N 问题是一个常见的性能问题。“1” 代表一次主查询,而 “N” 代表主查询结果集中每一条记录都要执行一次额外的查询。
具体来说,当你通过 JPA 执行一个查询获取实体列表,并且这些实体关联了其他实体(例如,通过 @ManyToOne
、@OneToOne
等注解),而 JPA 默认采用懒加载(Lazy Loading)策略时,在访问关联实体时就会触发额外的查询。
2. 示例场景
假设你有两个实体类:Order
(订单)和 Customer
(客户),一个订单对应一个客户,关系是 @ManyToOne
。
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Customer customer;
// getters and setters
}
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getters and setters
}
现在,你要查询所有订单,并打印出每个订单对应的客户名称:
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("yourPersistenceUnit");
EntityManager em = emf.createEntityManager();
// 执行一次主查询,获取所有订单
List<Order> orders = em.createQuery("SELECT o FROM Order o", Order.class).getResultList();
// 遍历订单列表,访问每个订单的客户信息
for (Order order : orders) {
System.out.println(order.getCustomer().getName());
}
em.close();
emf.close();
}
}
在上述代码中,首先执行一次查询获取所有订单(这是 “1”),然后在遍历订单列表时,对于每个订单,当访问其关联的客户信息时,由于默认是懒加载,会触发一次额外的查询来获取客户信息(这就是 “N”)。如果有 100 个订单,就会执行 1 + 100 次查询。
3. 解决方案
3.1 使用 JOIN FETCH
在 JPQL 中使用 JOIN FETCH
关键字,将关联实体一次性查询出来,避免额外的查询。
List<Order> orders = em.createQuery("SELECT o FROM Order o JOIN FETCH o.customer", Order.class).getResultList();
这样,在一次查询中就会将订单和关联的客户信息一起查询出来,不会再触发额外的查询。
3.2 使用 @BatchSize
注解
在关联实体的字段上使用 @BatchSize
注解,JPA 会将多个关联实体的查询合并成一个批量查询。
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.BatchSize;
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@BatchSize(size = 20)
private Customer customer;
// getters and setters
}
这里设置 @BatchSize(size = 20)
表示每次批量查询 20 个关联实体,减少查询次数。
3.3 使用 @Fetch(FetchMode.SUBSELECT)
注解
在关联实体的字段上使用 @Fetch(FetchMode.SUBSELECT)
注解,JPA 会使用子查询的方式一次性查询出所有关联实体。
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@Fetch(FetchMode.SUBSELECT)
private Customer customer;
// getters and setters
}
这样也可以避免 1 + N 问题,减少查询次数。