【问题标题】:Loading multiple entities by id efficiently in Hibernate在 Hibernate 中通过 id 有效加载多个实体
【发布时间】:2012-03-14 03:52:11
【问题描述】:

所以,我通过 id 获得了特定实体的多个实例:

for(Integer songId:songGroup.getSongIds()) {
   session = HibernateUtil.getSession();
   Song song = (Song) session.get(Song.class,id);
   processSong(song);
}

这会为每个 id 生成一个 SQL 查询,所以我想到我应该一次执行此操作,但除了运行查询外,我找不到在一次调用中获取多个实体的方法。所以我写了一个查询

return (List) session.createCriteria(Song.class)
       .add(Restrictions.in("id",ids)).list();

但是,如果我启用二级缓存并不意味着我的旧方法将能够从二级缓存返回对象(如果它们之前已被请求),但我的查询将始终转到数据库。

这样做的正确方法是什么?

【问题讨论】:

  • 你有没有想出办法做到这一点?

标签: java hibernate entity query-performance identifier


【解决方案1】:

在循环中一个一个地获取每个实体会导致N+1 query issues

因此,一次获取所有实体并在之后进行处理会效率更高。

现在,在您提出的解决方案中,您使用的是旧版 Hibernate Criteria,但由于它自 Hibernate 4 以来已被弃用,并且可能会在 Hibernate 6 中被删除,因此最好使用以下替代方案之一。

JPQL

您可以使用类似以下的 JPQL 查询:

List<Song> songs = entityManager
.createQuery(
    "select s " +
    "from Song s " +
    "where s.id in (:ids)", Song.class)
.setParameter("ids", songGroup.getSongIds())
.getResultList();

标准 API

如果您想动态构建查询,则可以使用 Criteria API 查询:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Song> query = builder.createQuery(Song.class);

ParameterExpression<List> ids = builder.parameter(List.class);

Root<Song> root = query
.from(Song.class);

query
.where(
    root.get("id").in(
        ids
    )
);

List<Song> songs = entityManager
.createQuery(query)
.setParameter(ids, songGroup.getSongIds())
.getResultList();

Hibernate 特定的 multiLoad

List<Song> songs = entityManager
.unwrap(Session.class)
.byMultipleIds(Song.class)
.multiLoad(songGroup.getSongIds());

现在,JPQL 和 Criteria API 也可以从 hibernate.query.in_clause_parameter_padding optimization 中受益,这允许您增加 SQL 语句缓存机制。

有关按标识符加载多个实体的更多详细信息,请查看this article

【讨论】:

    【解决方案2】:

    您在这里要求做的是让 Hibernate 为您的 Criteria 进行特殊情况处理,这需要提出很多要求。

    你必须自己做,但这并不难。使用SessionFactory.getCache(),您可以获得对缓存对象实际存储的引用。执行以下操作:

    for (Long id : allRequiredIds) {
      if (!sessionFactory.getCache().containsEntity(Song.class, id)) {
        idsToQueryDatabaseFor.add(id)
      } else {
        songs.add(session.get(Song.class, id));
      }
    }
    
    List<Song> fetchedSongs = session.createCriteria(Song.class).add(Restrictions.in("id",idsToQueryDatabaseFor).list();
    songs.addAll(fetchedSongs);
    

    然后从那里检索缓存中的歌曲,并使用单个 select 提取未提取的歌曲。

    【讨论】:

      【解决方案3】:

      如果您知道 ID 存在,则可以使用 load(..) 创建代理,而无需实际访问 DB:

      返回具有给定标识符的给定实体类的持久化实例,获取指定的锁模式,假设该实例存在。

      List<Song> list = new ArrayList<>(ids.size());
      for (Integer id : ids)
        list.add(session.load(Song.class, id, LockOptions.NONE));
      

      一旦您访问了非标识符访问器,Hibernate 将检查缓存并在需要时回退到 DB,如果已配置,则使用批处理。

      如果 ID 不存在,则在加载对象后会发生 ObjectNotFoundException。这可能在您的代码中您不会真正期望异常的某个地方 - 您最终使用的是一个简单的访问器。因此,要么 100% 确定 ID 存在,要么至少在您期望的位置尽早强制执行 ObjectNotFoundException,例如在填充列表之后。

      【讨论】:

        【解决方案4】:

        您可以做的另一件事是对 id 列表进行排序,识别连续 id 的子序列,然后在单个查询中查询这些子序列中的每一个。例如,给定List&lt;Long&gt; ids,请执行以下操作(假设您在 Java 中有一个 Pair 类):

        List<Pair> pairs=new LinkedList<Pair>();
        List<Object> results=new LinkedList<Object>();
        Collections.sort(ids);
        Iterator<Long> it=ids.iterator();
        
        Long previous=-1L;
        Long sequence_start=-1L;
        while (it.hasNext()){
            Long next=it.next();
        
            if (next>previous+1) {
                pairs.add(new Pair(sequence_start, previous));
                sequence_start=next;
            }
            previous=next;
        }
        pairs.add(new Pair(sequence_start, previous));
        
        for (Pair pair : pairs){
            Query query=session.createQuery("from Person p where p.id>=:start_id and p.id<=:end_id");
            query.setLong("start_id", pair.getStart());
            query.setLong("end_id", pair.getEnd());
        
            results.addAll((List<Object>)query.list());
        
        }
        

        【讨论】:

          【解决方案5】:

          休眠二级缓存与休眠查询缓存之间存在差异。 以下链接解释得很好:http://www.javalobby.org/java/forums/t48846.html

          简而言之, 如果您使用相同的参数多次使用相同的查询,那么您可以使用两者的组合来减少数据库命中。

          【讨论】:

            猜你喜欢
            • 2012-06-20
            • 2014-04-10
            • 2016-06-19
            • 1970-01-01
            • 1970-01-01
            • 2011-03-31
            • 1970-01-01
            • 2017-01-19
            • 1970-01-01
            相关资源
            最近更新 更多