提高持久层性能的最常见的建议之一是使用JOIN FETCH子句或EntityGraphs来获取加载实体时所需的关联。我完全同意这些建议,我们在持久化中心的Hibernate性能调整课程中详细讨论了这个问题。但是如果你遵循这个建议,调用setFirstResult 和setMaxResult 方法来限制结果集的大小,你会在日志文件中看到以下警告:
HHH000104: firstResult/maxResults是用collection fetch指定的;在内存中应用!。
如果你在使用JOIN FETCH子句或EntityGraph的查询上调用setFirstResult或setMaxResults方法,Hibernate 5会显示该警告。Hibernate 6改进了对EntityGraphs的处理,只有当你的查询包含JOIN FETCH子句时才会显示该警告。
内容
- 1为什么Hibernate会显示HH000104警告?
- 2如何避免HHH000104警告
- 3结论
为什么Hibernate会显示HH000104警告?
当你看一下Hibernate在使用JOIN FETCH 子句或EntityGraph时必须生成的SQL语句,这个警告的原因就很明显了。这两种方法都告诉Hibernate要初始化2个实体类之间的托管关联。要做到这一点,Hibernate需要连接相关的表并选择实体类映射的所有列。这将合并两个表中的记录并增加结果集的大小。如果你想通过调用setFirstResult和setMaxResults方法来限制其大小,这就会产生问题。
让我们来看看一个例子:

我在ChessTournament和ChessPlayer 实体类之间建立了一个多对多的关联。处理这个关联的最佳做法是使用默认的*FetchType.LAZY和JOIN FETCH子句或EntityGraph*来初始化它,如果需要的话。
然后Hibernate使用1条SQL语句获取所有需要的信息。但如果你限制了查询结果的大小,它就会触发之前显示的警告。你可以在下面的代码片断中看到一个例子:
TypedQuery<ChessTournament> q = em.createQuery("""
SELECT t
FROM ChessTournament t
LEFT JOIN FETCH t.players
WHERE t.name LIKE :name""",
ChessTournament.class);
q.setParameter("name", "%Chess%");
q.setFirstResult(0);
q.setMaxResults(5);
List<ChessTournament> tournaments = q.getResultList();
正如预期的那样,Hibernate在日志文件中写下了HH000104警告。它没有添加LIMIT或OFFSET子句来限制结果集的大小,尽管我把firstResult 设为0,maxResult 设为5:
15:56:57,623 WARN [org.hibernate.hql.internal.ast.QueryTranslatorImpl] - HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
15:56:57,626 DEBUG [org.hibernate.SQL] -
select
chesstourn0_.id as id1_1_0_,
chessplaye2_.id as id1_0_1_,
chesstourn0_.endDate as enddate2_1_0_,
chesstourn0_.name as name3_1_0_,
chesstourn0_.startDate as startdat4_1_0_,
chesstourn0_.version as version5_1_0_,
chessplaye2_.birthDate as birthdat2_0_1_,
chessplaye2_.firstName as firstnam3_0_1_,
chessplaye2_.lastName as lastname4_0_1_,
chessplaye2_.version as version5_0_1_,
players1_.ChessTournament_id as chesstou1_2_0__,
players1_.players_id as players_2_2_0__
from
ChessTournament chesstourn0_
left outer join
ChessTournament_ChessPlayer players1_
on chesstourn0_.id=players1_.ChessTournament_id
left outer join
ChessPlayer chessplaye2_
on players1_.players_id=chessplaye2_.id
where
chesstourn0_.name like ?
当你在SQL客户端中执行相同的语句时,其原因就显现出来了。通过加入托管关联并选择由ChessTournament 和ChessPlayer实体类映射的所有列,查询的结果集是ChessTournament 表中的记录和ChessPlayer 表中相关记录的乘积:
结果集中的每条记录都是一个锦标赛和其中一个棋手的唯一组合。这是关系型数据库处理这种查询的预期方式。但在JOIN FETCH子句或EntityGraph的特殊情况下,它产生了一个问题。
通常情况下,Hibernate使用firstResult 和maxResult 值来应用SQL语句中的分页。这些告诉数据库只返回结果集的一部分。在前面的例子中,我在调用setFirstResult 方法时使用了0,在调用setMaxResults 方法时使用了5。如果Hibernate对生成的SQL语句应用这些参数的标准处理,数据库将只返回结果集的前5行。正如你在下面的图片中看到的,这些记录包含了2021年塔塔钢铁国际象棋锦标赛的4名选手和2022年塔塔钢铁国际象棋锦标赛的1名选手:
但是这并不是我们的JPQL查询的目的。提供的firstResult和maxResult 值应该是返回前5个ChessTournament 实体和所有相关的ChessPlayer实体。他们应该为返回的ChessTournament实体对象定义分页,而不是为SQL结果集中的产品定义分页。
这就是为什么Hibernate将警告写入日志文件并在内存中应用分页。它执行SQL语句时没有任何分页。然后数据库会返回所有的ChessTournament实体和它们相关的ChessPlayers。而Hibernate在解析结果集时限制了返回的*List*的大小。
尽管这种方法提供了正确的结果,但它使你面临着严重的性能问题的风险。根据你的数据库的大小,查询可能会选择几千条记录,并拖慢你的应用程序。
如何避免HHH000104警告
避免Hibernate警告和潜在性能问题的最好方法是执行2个查询。第一个查询选择你要检索的所有ChessTournament 实体的主键。这个查询并不获取关联,你可以使用setFirstResult和setMaxResult 方法来限制结果集的大小。第2个是获取这些实体和它们相关的ChessPlayers:
TypedQuery<Long> idQuery = em.createQuery("""
SELECT t.id
FROM ChessTournament t
WHERE t.name LIKE :name""",
Long.class);
idQuery.setParameter("name", "%Chess%");
idQuery.setFirstResult(0);
idQuery.setMaxResults(5);
List<Long> tournamentIds = idQuery.getResultList();
TypedQuery<ChessTournament> tournamentQuery = em.createQuery("""
SELECT t
FROM ChessTournament t
LEFT JOIN FETCH t.players
WHERE t.id IN :ids""",
ChessTournament.class);
tournamentQuery.setParameter("ids", tournamentIds);
List<ChessTournament> tournaments = tournamentQuery.getResultList();
tournaments.forEach(t -> log.info(t));
前面的代码片段使用了Hibernate 6。如果你使用的是Hibernate 5,你应该在第2个查询中添加DISTINCT关键字,并将hibernate.query.passDistinctThrough的提示设置为false。正如我在之前关于Hibernate性能调整的文章中所解释的,这可以防止Hibernate为每个选手返回一个ChessTournament对象的引用:
TypedQuery<Long> idQuery = em.createQuery("""
SELECT t.id
FROM ChessTournament t
WHERE t.name LIKE :name""",
Long.class);
idQuery.setParameter("name", "%Chess%");
idQuery.setFirstResult(0);
idQuery.setMaxResults(5);
List<Long> tournamentIds = idQuery.getResultList();
TypedQuery<ChessTournament> tournamentQuery = em.createQuery("""
SELECT DISTINCT t
FROM ChessTournament t
LEFT JOIN FETCH t.players
WHERE t.id IN :ids""",
ChessTournament.class);
tournamentQuery.setParameter("ids", tournamentIds);
tournamentQuery.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);
List<ChessTournament> tournaments = tournamentQuery.getResultList();
这种方法可能看起来更复杂,要执行2条语句而不是1条,但它将查询结果集的分页与选手关联的初始化分开。这使得Hibernate能够将分页添加到第一条查询语句中,并防止它在内存中获取整个结果集并应用分页。这就解决了警告问题,如果你正在处理一个巨大的数据库,就可以提高你的应用程序的性能:
07:30:04,557 DEBUG [org.hibernate.SQL] -
select
c1_0.id
from
ChessTournament c1_0
where
c1_0.name like ? escape '' offset ? rows fetch first ? rows only
07:30:04,620 DEBUG [org.hibernate.SQL] -
select
c1_0.id,
c1_0.endDate,
c1_0.name,
p1_0.ChessTournament_id,
p1_1.id,
p1_1.birthDate,
p1_1.firstName,
p1_1.lastName,
p1_1.version,
c1_0.startDate,
c1_0.version
from
ChessTournament c1_0
left join
(ChessTournament_ChessPlayer p1_0
join
ChessPlayer p1_1
on p1_1.id=p1_0.players_id)
on c1_0.id=p1_0.ChessTournament_id
where
c1_0.id in(?,?,?)
07:30:04,666 INFO [com.thorben.janssen.sample.TestSample] - ChessTournament [id=1, name=Tata Steel Chess Tournament 2021, startDate=2021-01-14, endDate=2021-01-30, version=0]
07:30:04,666 INFO [com.thorben.janssen.sample.TestSample] - ChessTournament [id=2, name=Tata Steel Chess Tournament 2022, startDate=2022-01-14, endDate=2022-01-30, version=0]
07:30:04,666 INFO [com.thorben.janssen.sample.TestSample] - ChessTournament [id=3, name=2022 Superbet Chess Classic Romania, startDate=2022-05-03, endDate=2022-05-15, version=0]
结论
你应该使用JOIN FETCH条款或EntityGraphs来初始化你在业务代码中使用的关联。这可以避免n+1的选择问题,提高你的应用程序的性能。
但是如果你想通过调用setFirstResult和setMaxResult方法来限制结果集的大小,对关联实体的获取就会产生问题。然后,结果集包含了连接表中所有匹配记录的组合。如果Hibernate限制该结果集的大小,它将限制组合的数量而不是所选实体的数量。相反,它获取了整个结果集,并在内存中应用分页。根据结果集的大小,这可能导致严重的性能问题。
你可以通过执行两个查询语句来避免这种情况。第一条语句在获取你想要检索的所有记录的主键时应用分页。在这篇文章的例子中,这些是符合WHERE子句的所有ChessTournament 实体的id值。第二个查询使用主键值的列表来获取实体对象,并初始化所需的关联。

