如何避免警告“使用集合提取指定的 firstResult/maxResults;在内存中应用!”啥时候使用休眠?
Posted
技术标签:
【中文标题】如何避免警告“使用集合提取指定的 firstResult/maxResults;在内存中应用!”啥时候使用休眠?【英文标题】:How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?如何避免警告“使用集合提取指定的 firstResult/maxResults;在内存中应用!”什么时候使用休眠? 【发布时间】:2012-07-11 00:18:45 【问题描述】:我在服务器日志中收到警告“firstResult/maxResults specified with collection fetch;apply in memory!”。但是一切正常。但我不想要这个警告。
我的代码是
public employee find(int id)
return (employee) getEntityManager().createQuery(QUERY).setParameter("id", id).getSingleResult();
我的查询是
QUERY = "from employee as emp left join fetch emp.salary left join fetch emp.department where emp.id = :id"
【问题讨论】:
请在您构建和执行查询的位置添加代码。 JMelnik,请使用代码和查询查看我编辑的问题。 查询也可以在哪里设置 maxResult 参数。 这是我的应用程序中的代码和查询。我没有手动设置 maxResult。 这意味着 Hibernate 正在获取所有内容,然后尝试在内存中应用第一个/最大结果限制。这对您来说可能看起来不受欢迎。以下是 JPA 规范对这种交互的说明:“将setMaxResults
或 setFirstResult
应用于涉及集合上的 fetch 连接的查询的效果是未定义的。(JPA "Enterprise JavaBeans 3.0, Final Release",Kapitel 3.6.1 查询接口)”
【参考方案1】:
出现此警告的原因是,当使用 fetch join 时,结果集中的顺序仅由所选实体的 ID 定义(而不是由 join fetched 定义)。
如果内存中的这种排序导致问题,请不要将 firsResult/maxResults 与 JOIN FETCH 一起使用。
【讨论】:
【参考方案2】:为避免此警告,您必须将调用 getSingleResult
更改为
getResultList().get(0)
【讨论】:
虽然投票可能已经表明了这一点:这确实是真的。即使对于不使用任何分页的查询,例如setFirstResult(offset)
或setMaxResults(limit)
,Hibernate 仍可能在使用getSingleResult
时抱怨这一点。 (我认为 Hibernate 在内部是有限制的。)
一个显着的区别是,当您希望返回 1 个结果的查询实际上返回了多个结果时,这不会引发异常。我会添加某种断言。
尽管已经使用了 getResultList(),但我收到了 JOIN FETCH 警告。所以这不可能是解决这个问题的唯一潜在解决方案。【参考方案3】:
问题是你会得到笛卡尔积做 JOIN。如果您仍然在同一个根身份类上,偏移量将减少您的记录集
【讨论】:
【参考方案4】:我猜 emp 有很多部门,这是一对多的关系。 Hibernate 将使用获取的部门记录获取此查询的许多行。因此,在它真正将结果提取到内存之前,无法确定结果集的顺序。所以分页会在内存中完成。
如果你不想用emp获取部门,但仍想根据部门做一些查询,你可以达到没有警告的结果(不做内存排序)。为此,您必须删除“获取”子句。所以像下面这样:
QUERY = "从员工作为 emp left join emp.salary sal left join emp.department dep where emp.id = :id and dep.name = 'testing' and sal.salary > 5000 "
【讨论】:
【参考方案5】:虽然您获得了有效的结果,但 SQL 查询会获取所有数据,但效率并没有达到应有的水平。
所以,你有两个选择。
解决了两个可以在读写模式下获取实体的 SQL 查询的问题
解决此问题的最简单方法是执行两个查询:
。第一个查询将获取与提供的过滤条件匹配的根实体标识符。 .第二个查询将使用之前提取的根实体标识符来获取父实体和子实体。
这种方法很容易实现,如下所示:
List<Long> postIds = entityManager
.createQuery(
"select p.id " +
"from Post p " +
"where p.title like :titlePattern " +
"order by p.createdOn", Long.class)
.setParameter(
"titlePattern",
"High-Performance Java Persistence %"
)
.setMaxResults(5)
.getResultList();
List<Post> posts = entityManager
.createQuery(
"select distinct p " +
"from Post p " +
"left join fetch p.comments " +
"where p.id in (:postIds) " +
"order by p.createdOn", Post.class)
.setParameter("postIds", postIds)
.setHint(
"hibernate.query.passDistinctThrough",
false
)
.getResultList();
解决了一个只能在只读模式下获取实体的 SQL 查询的问题
第二种方法是对匹配我们过滤条件的父实体和子实体的结果集使用 SDENSE_RANK,并仅限制前 N 个帖子条目的输出。
SQL 查询如下所示:
@NamedNativeQuery(
name = "PostWithCommentByRank",
query =
"SELECT * " +
"FROM ( " +
" SELECT *, dense_rank() OVER (ORDER BY \"p.created_on\", \"p.id\") rank " +
" FROM ( " +
" SELECT p.id AS \"p.id\", " +
" p.created_on AS \"p.created_on\", " +
" p.title AS \"p.title\", " +
" pc.id as \"pc.id\", " +
" pc.created_on AS \"pc.created_on\", " +
" pc.review AS \"pc.review\", " +
" pc.post_id AS \"pc.post_id\" " +
" FROM post p " +
" LEFT JOIN post_comment pc ON p.id = pc.post_id " +
" WHERE p.title LIKE :titlePattern " +
" ORDER BY p.created_on " +
" ) p_pc " +
") p_pc_r " +
"WHERE p_pc_r.rank <= :rank ",
resultSetMapping = "PostWithCommentByRankMapping"
)
@SqlResultSetMapping(
name = "PostWithCommentByRankMapping",
entities =
@EntityResult(
entityClass = Post.class,
fields =
@FieldResult(name = "id", column = "p.id"),
@FieldResult(name = "createdOn", column = "p.created_on"),
@FieldResult(name = "title", column = "p.title"),
),
@EntityResult(
entityClass = PostComment.class,
fields =
@FieldResult(name = "id", column = "pc.id"),
@FieldResult(name = "createdOn", column = "pc.created_on"),
@FieldResult(name = "review", column = "pc.review"),
@FieldResult(name = "post", column = "pc.post_id"),
)
)
@NamedNativeQuery
获取与提供的标题匹配的所有 Post 实体及其关联的 PostComment
子实体。 DENSE_RANK
窗口函数用于为每个 Post 和 PostComment
加入的记录分配排名,以便我们以后可以过滤我们有兴趣获取的 Post 记录的数量。
SqlResultSetMapping
提供 SQL 级列别名和需要填充的 JPA 实体属性之间的映射。
现在,我们可以像这样执行PostWithCommentByRank
@NamedNativeQuery
:
List<Post> posts = entityManager
.createNamedQuery("PostWithCommentByRank")
.setParameter(
"titlePattern",
"High-Performance Java Persistence %"
)
.setParameter(
"rank",
5
)
.unwrap(NativeQuery.class)
.setResultTransformer(
new DistinctPostResultTransformer(entityManager)
)
.getResultList();
现在,默认情况下,像 PostWithCommentByRank
这样的原生 SQL 查询会在同一 JDBC 行中获取 Post 和 PostComment
,因此我们最终会得到一个包含两个实体的 Object[]
。
但是,我们希望将表格Object[]
数组转换为父子实体树,因此,我们需要使用 Hibernate ResultTransformer
。
DistinctPostResultTransformer
如下所示:
public class DistinctPostResultTransformer
extends BasicTransformerAdapter
private final EntityManager entityManager;
public DistinctPostResultTransformer(
EntityManager entityManager)
this.entityManager = entityManager;
@Override
public List transformList(
List list)
Map<Serializable, Identifiable> identifiableMap =
new LinkedHashMap<>(list.size());
for (Object entityArray : list)
if (Object[].class.isAssignableFrom(entityArray.getClass()))
Post post = null;
PostComment comment = null;
Object[] tuples = (Object[]) entityArray;
for (Object tuple : tuples)
if(tuple instanceof Identifiable)
entityManager.detach(tuple);
if (tuple instanceof Post)
post = (Post) tuple;
else if (tuple instanceof PostComment)
comment = (PostComment) tuple;
else
throw new UnsupportedOperationException(
"Tuple " + tuple.getClass() + " is not supported!"
);
if (post != null)
if (!identifiableMap.containsKey(post.getId()))
identifiableMap.put(post.getId(), post);
post.setComments(new ArrayList<>());
if (comment != null)
post.addComment(comment);
return new ArrayList<>(identifiableMap.values());
DistinctPostResultTransformer
必须分离正在获取的实体,因为我们正在覆盖子集合并且我们不希望它作为实体状态转换传播:
post.setComments(new ArrayList<>());
【讨论】:
第二个查询(加载“帖子”的那个)不是缺少排序吗?我们可以依赖数据库以与给定 ID 相同的顺序返回帖子吗?我认为也可以简单地将order by p.createdOn
添加到第二个查询中......
帖子已经按照第一个查询提供的正确顺序获取,您可以使用 List
引用来保留顺序。第二个查询只是将未初始化的子关联替换为非代理 List
。
可能取决于数据库?在 Postgres 中,我还必须将 ORDER BY 子句添加到第二个查询中。 ***.com/q/866465/716834 似乎也证实了这一点......
有谁知道如何使用自定义谓词进行这项工作?我正在尝试建立一个搜索端点。但似乎我不能一次将自定义谓词、页面请求和本机 SQL 组合在一起......【参考方案6】:
这个警告告诉你 Hibernate 正在内存中执行 java 分页。这可能会导致高 JVM 内存消耗。 由于开发人员可能会错过此警告,因此我通过添加允许抛出异常而不是记录警告的标志来为 Hibernate 做出贡献 (https://hibernate.atlassian.net/browse/HHH-9965)。
标志是 hibernate.query.fail_on_pagination_over_collection_fetch。
我建议大家启用它。
标志在 org.hibernate.cfg.AvailableSettings 中定义:
/**
* Raises an exception when in-memory pagination over collection fetch is about to be performed.
* Disabled by default. Set to true to enable.
*
* @since 5.2.13
*/
String FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH = "hibernate.query.fail_on_pagination_over_collection_fetch";
【讨论】:
【参考方案7】:正如其他人指出的那样,您通常应避免同时使用“JOIN FETCH”和 firstResult/maxResults。
如果您的查询需要,您可以使用.stream()
来消除警告并避免潜在的OOM 异常。
try (Stream<ENTITY> stream = em.createQuery(QUERY).stream())
ENTITY first = stream.findFirst().orElse(null); // equivalents .getSingleResult()
//Stream
返回的是一个IO流,需要手动关闭。
【讨论】:
以上是关于如何避免警告“使用集合提取指定的 firstResult/maxResults;在内存中应用!”啥时候使用休眠?的主要内容,如果未能解决你的问题,请参考以下文章