使用 JPA 获取集合时避免 N+1 和笛卡尔积问题的标准方法是啥
Posted
技术标签:
【中文标题】使用 JPA 获取集合时避免 N+1 和笛卡尔积问题的标准方法是啥【英文标题】:What is the standard way to avoid both N+1 and the Cartesian Product issues while fetching collections with JPA使用 JPA 获取集合时避免 N+1 和笛卡尔积问题的标准方法是什么 【发布时间】:2014-09-15 09:49:45 【问题描述】:当一个实体的字段部分是集合时,人们希望以尽可能少的查询次数和尽可能少的内存来获取数据。
第一个问题通过 JPQL 查询中的“join fetch”解决(解决 N+1 问题)。
但是“join fetch”(通过检查相应的 SQL 查询很容易看出)导致笛卡尔积问题:对应于没有多重性的实体字段的每个“行”都存在于返回的结果集中,具有多重性 N_1 x N_2 x ... x N_m,其中 N_1 是第一个集合的重数,N_2 是第二个集合的重数,N_m 是第 m 个集合的重数,假设实体有 m 个字段是集合。
Hibernate 使用 FetchMode.SUBSELECT 解决了这个问题(如果我没记错的话,它会进行 m+1 次查询,每个查询都不会返回冗余数据)。在 JPA 中解决此问题的 标准 方法是什么(在我看来,至少在这种情况下,我不能将 JPA 注释与 Hibernate 的注释混合)?
【问题讨论】:
实际上,我认为FetchMode.SUBSELECT
进行了两个查询:一个检索父级,一个通过IN 查询检索关联的子级。
@Tim 我用 m 表示作为集合的实体字段的数量,而不是这些集合的基数(我澄清它以防我的符号误导)。如果我没记错的话,Hibernate 将执行一个查询来检索父级,并为 m 个集合中的每一个加上一个带有 IN 的查询;因此,总共有 m+1 个查询。
你说得对,我看错了你写的东西。
【参考方案1】:
最佳方法是用查询替换集合,尤其是当预期大小大到足以导致性能问题时:
您删除双向@OneToMan
y 端,只留下拥有@ManyToOne
端
您选择父实体(例如Country
)运行如下查询:
select c from City c where c.country = :country
select c from County c where c.country = :country
select count(p), c.name from People p join p.country group by c.name
【讨论】:
我稍微改变了措辞。总而言之,无限大小的集合会导致严重的性能问题,因此目前没有标准的方法来防止这种情况。【参考方案2】:我尝试添加 @fetch(FetchMode.SUBSELECT) 或 @fetch(FetchMode.SELECT) 它没有做任何更改,即(仍然进行连接而不进行子选择两个查询,它使所有查询都在同一个查询中选择)
【讨论】:
抱歉现在才回复;尝试看一下mkyong.com/hibernate/hibernate-fetching-strategies-examples,应该有一个@Fetch(FetchMode.SUBSELECT) 的工作示例【参考方案3】:当你尝试使用时
entityManager.find(PoDetail.class, poNumber)
您将拥有在具有@OneToMany
的实体中声明的所有列表,这些列表使用笛卡尔积初始化,其中也将有重复项。当然可以通过使用Set
来消除这些重复,但是 Set 不会保留数据插入的顺序,当尝试在 View 中显示时,我们会打乱行。
我通过使用解决了这个问题:
带参数的NamedQueries,以避免此类获取集合的笛卡尔积。
这样做,您的视图数据将与持久数据插入顺序保持一致。
这里是示例代码:
父实体类:(它有更多的列表字段,我在这里提到一个)
@Entity
@Table(name="PO_DETAILS")
@NamedQuery(name="PoDetail.findByPoNumber", query="SELECT p FROM PoDetail p where p.poNumber=:poNumber")
public class PoDetail implements Serializable
@Id
@Column(name="PO_NUMBER", unique=true, nullable=false, length=30)
private String poNumber;
@Column(name="ACTION_TAKEN", length=2000)
private String actionTaken;
.....
//bi-directional one-to-many association to PcrDetail
@OneToMany(mappedBy="poDetail", cascade=CascadeType.ALL, fetch=FetchType.EAGER, orphanRemoval=true)
private List<PcrDetail> pcrDetails;
子实体类:
@Entity
@Table(name="PCR_DETAILS")
public class PcrDetail implements Serializable
@Id
@Column(name="PCR_NUMBER", unique=true, nullable=false, length=30)
private String pcrNumber;
@Column(name="CONTRACT_ID", length=30)
private String contractId;
.....
//bi-directional many-to-one association to PoDetail
@ManyToOne(cascade=CascadeType.ALL)
@JoinColumn(name="PARENT_PO_NUMBER", insertable=false, updatable=false)
private PoDetail poDetail;
JPA DAO 类:
public PoBean getPoDetails(PoBean poBean)
PoDetail poDetail = poBean.getPoDetail();
String poNumber = poDetail.getPoNumber();
entityManagerFactory = JpaUtil.getEntityManagerFactory();
entityManager = entityManagerFactory.createEntityManager();
try
try
poDetail = (PoDetail) entityManager
.createNamedQuery("PoDetail.findByPoNumber")
.setParameter("poNumber", poNumber).getSingleResult();
catch (NoResultException nre)
poBean.setStatusCode(PopVO.ERROR);
poBean.setErrorMessage("No PO details foun with PO Number : "
+ poNumber);
return poBean;
catch (Exception e)
e.printStackTrace();
poBean.setStatusCode(PopVO.ERROR);
poBean.setErrorMessage(e.getMessage());
return poBean;
finally
entityManager.close();
【讨论】:
在我看来,您误解了笛卡尔积问题是什么;例如,尝试查看learningviacode.blogspot.it/2012/08/…。 这是一个错误的答案。您应该在一对多映射中使用延迟加载。使用 Eager 是一种不好的做法以上是关于使用 JPA 获取集合时避免 N+1 和笛卡尔积问题的标准方法是啥的主要内容,如果未能解决你的问题,请参考以下文章
JPA 一个实体中的两个惰性集合 - 如何运行 JPA 查询以获取实体和只有一个集合