使用 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】:

最佳方法是用查询替换集合,尤其是当预期大小大到足以导致性能问题时:

    您删除双向@OneToMany 端,只留下拥有@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 查询以获取实体和只有一个集合

笛卡尔积

如何在 Pandas Python 中合并时避免笛卡尔坐标

多表查询

Spring Data JPA - 在没有 @Transactional 的情况下获取延迟加载的集合

JPA 同步实体访问器