提高使用JPA/Hibernate查询树形结构时的性能

mnemlml8  于 2022-09-18  发布在  Java
关注(0)|答案(1)|浏览(260)

我们在Oracle中有这四个持久化树状结构的表,并且我们正在使用Hibernate来处理持久化这项肮脏的工作。它在几乎所有场景中都工作得很好,除了查询一组这样的结构以将其作为JSON传输之外。检索3k个实体大约需要30秒,所以我尝试进一步调查。

实体如下所示(为简洁起见,省略了许多代码):

@Table(name = "ROOT")
public class Root {
    @Id
    private Long id;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "root")
    @Fetch(org.hibernate.annotations.FetchMode.SUBSELECT)
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private List<Stem> stems;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "root")
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private List<Branch> branches;
}

@Table(name = "STEM")
public class Stem {
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "ROOT_ID")
    private Root root;

}

@Table(name = "BRANCH")
public class Branch {
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "ROOT_ID")
    private Root root;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "branch")
    private List<Leaf> leaves;
}

@Table(name = "LEAF")
public class Leaf {
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = "BRANCH_ID")
    private Branch branch;
}

这些测试是在25个e1d0d1、略低于200个e1d1d1和略多于3k个e1d2d1的情况下进行的。此外,还有2500个e1d3d1出席。

阅读日志,对我来说,Hibernate会产生DFS,这会导致大量的查询(显然,每个实体都有一个)被触发。

因此,我尝试通过为四个表中的每个表触发一个查询来实现一个BFS(为简洁起见,在Python中)。令人惊讶的是,我能够在不到半秒的时间内准备好相同的JSON(包括json.dump的全部时间)!脚本是这样的(所有关系都是这样重复的):

cur.execute('select * from PARENT_TABLE')
parents = parent_tuples_to_dict(cur.fetchmany(25))

cur.execute(f'select * from CHILD_TABLE where PARENT_ID in ({",".join([p[id] for p in parents])})')
children = child_tuples_to_dict(cur.fetchall())

for child in children.values():
  parent = next(p for p in parents if p['id'] == child['parent_id'])
  parent['children'].append(child)

我正在考虑只更改DAO来执行BFS,使用@SqlResultSetMapping原生查询,但这看起来是一个相当难看的解决方案。此外,我认为我需要自己管理实体以防止内存泄漏。

我的问题是:

**问题1:**会不会是我们做错了什么导致这种表现的原因(我试图恢复关于实体的所有相关细节,但如果需要,我可以带来更多细节)?

**问题#2:**使用JPA/Hibernate/命名查询等进行这种查询的正确/好方法是什么?

**问题#3:**如果“手动”处理真的成为最有效的解决方案,我是否真的需要“手动”管理实体,以防止内存泄漏?

zu0ti5jz

zu0ti5jz1#

我的解决方案

所以..。我在Facebook小组的一些同事的帮助下发现,这是一种称为N+1查询的问题,在使用ORM时,这是一种常见的问题

我的解决方案是使用本文中描述的关于multiple bag fetch exception的技巧来利用持久化上下文行为,并获得与我使用Python脚本获得的结果类似的结果。

我是如何解决它的

基本上,一旦我加载了父表(将子表标记为惰性),我就会在对要加载的子表执行连接提取时,再次选择那些项。

对于我在问题中发布的示例,解决方案如下所示:

void fetchWholeTree(List<Root> roots) {
    em.createNamedQuery("select distinct r " +
                        "from Root r " +
                        "left join fetch r.stems s " +
                        "where r in :roots")
            .setParameter("roots", roots)
            .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
            .getResultList();
    em.createNamedQuery("select distinct r " +
                        "from Root r " +
                        "left join fetch r.branches b " +
                        "where r in :roots")
            .setParameter("roots", roots)
            .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
            .getResultList();
    em.createNamedQuery("select distinct b " +
                        "from Root r " +
                        "left join r.branches b " +
                        "left join fetch b.leaves l " +
                        "where r in :roots")
            .setParameter("roots", roots)
            .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
            .getResultList();
}

尝试回答我留下的问题

问题#1:是的。我们忽略了N+1个查询方案。

问题#2:看起来使用Hibernate解决N+1查询的正确方法之一是使用Hibernate::initialize@Fetch(FetchMode.SUBSELECT),但我无法找到如何解决。

问题#3:我没有时间找出它,我有一种强烈的感觉,只使用@SqlResultSetMapping就可以充分利用持久性上下文。如果我曾经尝试过,我会用结果编辑这个答案。

相关问题