连接返回许多部分重复项的高效 Hibernate 标准

Efficient Hibernate criteria for join returning many partial duplicates

我正在获取一长串实体,这些实体指的是其他实体,这些实体指的是......最后,通常所有实体都将单个 user 作为它们的 owner .并不奇怪,因为查询的是属于单个 user 的实体。多行重复的部分较多;实际上,只有一小部分是独特的数据。由于查询似乎很慢,我虽然可以通过使用

单独获取东西来获得一些好处
criteria.setFetchMode(path, FetchMode.SELECT);

这在我的上述情况下有效,但是当查询许多用户(作为管理员)时,它变得很糟糕,因为休眠会针对 every user 发出单独的查询,而不是像

SELECT * FROM User WHERE id IN (?, ?, ..., ?)

根本不获取它们(这比每个实体一个查询更糟糕)。我想知道我错过了什么?

因此,我 运行 进入 1+N 问题,而不是获取大量冗余数据,显然 1+1 查询就可以了。


我认为这不重要,但我的类就像

class Child {
    @ManyToOne Father father;
    @ManyToOne Mother mother;
    ...
}
class Father {
    @ManyToOne User owner;
    ...
}
class Mother {
    @ManyToOne User owner;
    ...
}

查询就像

createCriteria(Child.class)
.add(Restrictions.in("id", idList))
.add(Restrictions.eq("isDeleted", false))

.createAlias("Father", "f")
.add(Restrictions.eq("f.isDeleted", false))
.setFetchMode("f.owner", FetchMode.SELECT)

.createAlias("Mother", "m")
.add(Restrictions.eq("m.isDeleted", false))
.setFetchMode("m.owner", FetchMode.SELECT)

.list();

重要的部分是 owner 不会被使用并且可以被代理。 FetchMode.SELECT 的 javadoc 说

Fetch eagerly, using a separate select

所以它基本上承诺我想要的 1+1 查询而不是 "using a separate select per entity"。

我写了一个小项目来演示这个行为。根据您的条件生成的 SQL 如下:

select
    this_.id as id1_0_4_,
    this_.father_id as father_i3_0_4_,
    this_.isDeleted as isDelete2_0_4_,
    this_.mother_id as mother_i4_0_4_,
    f1_.id as id1_1_0_,
    f1_.isDeleted as isDelete2_1_0_,
    f1_.owner_id as owner_id3_1_0_,
    user5_.id as id1_3_1_,
    user5_.isDeleted as isDelete2_3_1_,
    m2_.id as id1_2_2_,
    m2_.isDeleted as isDelete2_2_2_,
    m2_.owner_id as owner_id3_2_2_,
    user7_.id as id1_3_3_,
    user7_.isDeleted as isDelete2_3_3_ 
from
    Child this_ 
inner join
    Father f1_ 
        on this_.father_id=f1_.id 
left outer join
    User user5_ 
        on f1_.owner_id=user5_.id 
inner join
    Mother m2_ 
        on this_.mother_id=m2_.id 
left outer join
    User user7_ 
        on m2_.owner_id=user7_.id 
where
    this_.id in (
        ?, ?
    ) 
    and this_.isDeleted=? 
    and f1_.isDeleted=? 
    and m2_.isDeleted=?
  1. 更改条件中的 FetchMode API 不会影响查询。仍然查询所有者数据。
  2. Id 在 "in" 子句中,Hibernate 没有为每个 Id 发出单独的查询。

如其他答案中所述,如果实体关系设置为EAGER,即JPA 默认值,则无法在Criteria API 中更改获取模式。 fetch mode需要改成LAZY.

可以看到here

Fetch profiles are meant to help you achieve what you want, but are very limited at the time being and you can override the default fetch plan/strategy only with the join-style fetch profiles (you can make a lazy association eager, but not vice versa). However, you could use this trick 反转该行为:默认设置惰性关联并默认为所有 sessions/transactions 启用配置文件。然后禁用您想要延迟加载的事务中的配置文件。

恕我直言,上面的解决方案看起来太麻烦了,我在大多数用例中使用的方法来避免加载冗余数据和 N+1 选择问题是使关联延迟并定义

总结一下我的挫败感……Hibernate 在这方面充满了惊喜(错误?):

  • 除非 属性 是用 @ManyToOne(fetch=FetchType.LAZY) 声明的,否则您无法更改任何内容
  • 默认是FetchType.EAGER,这是愚蠢的,因为它不能被覆盖
  • 使用 criteria.setFetchMode(path, FetchMode.SELECT) 毫无意义 因为它始终是空操作(由于 属性 或属性 已经很懒了)!
  • 延迟获取默认会导致 1+N 问题
  • 它可以通过 class 级 @BatchSize 注释进行控制
  • 在标量字段上放置 @BatchSize 注释会被静默忽略

为了得到我想要的(两个 SQL 查询),我只需要两件事:

  • @ManyToOne(fetch=FetchType.LAZY)
  • 声明 属性
  • @BatchSize(size=aLot) 放在 属性
  • 的 class 上

这很简单,但有点难以找出(因为上面所有被忽略的事情)。我还没有研究获取配置文件。

unless the property is declared with @ManyToOne(fetch=FetchType.LAZY), you can't change anything

是的,至少目前是这样,直到扩展获取配置文件功能以提供将预加载更改为延迟加载的能力。

the default is FetchType.EAGER, which is stupid, as it can't be overridden

是的,我同意这很糟糕,但是在 Hibernate native API 默认情况下一切都是惰性的;除非另有明确说明,否则 JPA 要求一对一关联是热切的。

using criteria.setFetchMode(path, FetchMode.SELECT) is pointless as it's always a no-op (either it gets ignored because of the non-overridable eagerness of the property or the property is lazy already)!

有了它,您应该能够覆盖其他延迟获取模式。请参阅来自一位主要 Hibernate 贡献者的关于 javadoc 混淆的 HHH-980 and this comment

fetching lazily leads by default to the 1+N problem

它与延迟加载无关,如果您不在同一查询中获取预加载关联,它也是预加载的默认设置。

it can be controlled via a class-level @BatchSize annotation

您必须将其放置在 class 层级才能使其对与该实体的一对一关联生效; this answer 很有帮助。对于集合关联(与在其他实体中定义的该实体的对多关联),您可以灵活地为每个关联单独定义它。