在 Hibernate 中进行分页是不是有比执行选择和计数查询更有效的方法?

Posted

技术标签:

【中文标题】在 Hibernate 中进行分页是不是有比执行选择和计数查询更有效的方法?【英文标题】:Is there a more efficient way of making pagination in Hibernate than executing select and count queries?在 Hibernate 中进行分页是否有比执行选择和计数查询更有效的方法? 【发布时间】:2010-09-15 03:33:56 【问题描述】:

通常分页查询看起来像这样。有没有更好的方法,而不是制作两个几乎相等的方法,一个执行“select *...”,另一个执行“count *...”?

public List<Cat> findCats(String name, int offset, int limit) 

    Query q = session.createQuery("from Cat where name=:name");

    q.setString("name", name);

    if (offset > 0) 
        q.setFirstResult(offset);
    
    if (limit > 0) 
        q.setMaxResults(limit);
    

    return q.list();



public Long countCats(String name) 
    Query q = session.createQuery("select count(*) from Cat where name=:name");
    q.setString("name", name);
    return (Long) q.uniqueResult();

【问题讨论】:

你可以像凯尔建议的那样在不知道数据库中有多少数据的情况下获取一页数据,但是如果你需要显示类似“返回X结果,显示yz”的内容,那么为了获得X,您必须运行计数查询。对不起。 【参考方案1】:

Here is a solution Richard Kennard 博士(请注意博客评论中的错误修复!),使用 Hibernate Interceptors

总而言之,您将 sessionFactory 绑定到您的拦截器类,以便您的拦截器稍后可以为您提供找到的行数。

您可以在解决方案链接上找到代码。下面是一个示例用法。

SessionFactory sessionFactory = ((org.hibernate.Session) mEntityManager.getDelegate()).getSessionFactory();
mysqlCalcFoundRowsInterceptor foundRowsInterceptor = new MySQLCalcFoundRowsInterceptor( sessionFactory );
Session session = sessionFactory.openSession( foundRowsInterceptor );

try 
   org.hibernate.Query query = session.createQuery( ... )   // Note: JPA-QL, not createNativeQuery!
   query.setFirstResult( ... );
   query.setMaxResults( ... );

   List entities = query.list();
   long foundRows = foundRowsInterceptor.getFoundRows();

   ...

 finally 

   // Disconnect() is good practice, but close() causes problems. Note, however, that
   // disconnect could lead to lazy-loading problems if the returned list of entities has
   // lazy relations

   session.disconnect();

【讨论】:

【参考方案2】:

我的解决方案适用于非常常见的 Hibernate+Spring+MySQL 用例

与上述答案类似,我的解决方案基于Dr Richard Kennar's。然而,由于 Hibernate 经常与 Spring 一起使用,我希望我的解决方案能够很好地与 Spring 以及使用 Hibernate 的标准方法一起使用。因此,我的解决方案使用线程本地和单例 bean 的组合来实现结果。从技术上讲,拦截器在为 SessionFactory 准备的每个 SQL 语句上调用,但它会跳过所有逻辑并且不初始化任何 ThreadLocal(s),除非它是专门设置为计算总行数的查询。

使用下面的类,您的 Spring 配置如下所示:

<bean id="foundRowCalculator" class="my.hibernate.classes.MySQLCalcFoundRowsInterceptor" />
    <!-- p:sessionFactoryBeanName="mySessionFactory"/ -->

<bean id="mySessionFactory"
    class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
    p:dataSource-ref="dataSource"
    p:packagesToScan="my.hibernate.classes"
    p:entityInterceptor-ref="foundRowCalculator"/>

基本上你必须声明拦截器bean,然后在SessionFactoryBean的“entityInterceptor”属性中引用它。如果您的 Spring 上下文中有多个 SessionFactory 并且您要引用的会话工厂不称为“sessionFactory”,则必须仅设置“sessionFactoryBeanName”。无法设置引用的原因是这会导致无法解析的 bean 之间的相互依赖关系。

对结果使用包装器 bean:

package my.hibernate.classes;

public class PagedResponse<T> 
    public final List<T> items;
    public final int total;
    public PagedResponse(List<T> items, int total) 
        this.items = items;
        this.total = total;
    

然后使用抽象基 DAO 类,您必须在进行查询之前调用“setCalcFoundRows(true)”,并在 [在 finally 块中以确保它被调用]之后调用“reset()”:

package my.hibernate.classes;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.springframework.beans.factory.annotation.Autowired;

public abstract class BaseDAO 

    @Autowired
    private MySQLCalcFoundRowsInterceptor rowCounter;

    public <T> PagedResponse<T> getPagedResponse(Criteria crit, int firstResult, int maxResults) 
        rowCounter.setCalcFoundRows(true);
        try 
            @SuppressWarnings("unchecked")
            return new PagedResponse<T>(
                crit.
                setFirstResult(firstResult).
                setMaxResults(maxResults).
                list(),
                rowCounter.getFoundRows());
         finally 
            rowCounter.reset();
        
    

    public <T> PagedResponse<T> getPagedResponse(Query query, int firstResult, int maxResults) 
        rowCounter.setCalcFoundRows(true);
        try 
            @SuppressWarnings("unchecked")
            return new PagedResponse<T>(
                query.
                setFirstResult(firstResult).
                setMaxResults(maxResults).
                list(),
                rowCounter.getFoundRows());
         finally 
            rowCounter.reset();
        
    

然后是一个名为 MyEntity 的 @Entity 的具体 DAO 类示例,它具有字符串属性“prop”:

package my.hibernate.classes;

import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions
import org.springframework.beans.factory.annotation.Autowired;

public class MyEntityDAO extends BaseDAO 

    @Autowired
    private SessionFactory sessionFactory;

    public PagedResponse<MyEntity> getPagedEntitiesWithPropertyValue(String propVal, int firstResult, int maxResults) 
        return getPagedResponse(
            sessionFactory.
            getCurrentSession().
            createCriteria(MyEntity.class).
            add(Restrictions.eq("prop", propVal)),
            firstResult, 
            maxResults);
    

最后是完成所有工作的拦截器类:

package my.hibernate.classes;

import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.hibernate.EmptyInterceptor;
import org.hibernate.HibernateException;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.jdbc.Work;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

public class MySQLCalcFoundRowsInterceptor extends EmptyInterceptor implements BeanFactoryAware 



    /**
     * 
     */
    private static final long serialVersionUID = 2745492452467374139L;

    //
    // Private statics
    //

    private final static String SELECT_PREFIX = "select ";

    private final static String CALC_FOUND_ROWS_HINT = "SQL_CALC_FOUND_ROWS ";

    private final static String SELECT_FOUND_ROWS = "select FOUND_ROWS()";

    //
    // Private members
    //
    private SessionFactory sessionFactory;

    private BeanFactory beanFactory;

    private String sessionFactoryBeanName;

    private ThreadLocal<Boolean> mCalcFoundRows = new ThreadLocal<Boolean>();

    private ThreadLocal<Integer> mSQLStatementsPrepared = new ThreadLocal<Integer>() 
        @Override
        protected Integer initialValue() 
            return Integer.valueOf(0);
        
    ;

    private ThreadLocal<Integer> mFoundRows = new ThreadLocal<Integer>();



    private void init() 
        if (sessionFactory == null) 
            if (sessionFactoryBeanName != null) 
                sessionFactory = beanFactory.getBean(sessionFactoryBeanName, SessionFactory.class);
             else 
                try 
                    sessionFactory = beanFactory.getBean("sessionFactory", SessionFactory.class);
                 catch (RuntimeException exp) 

                
                if (sessionFactory == null) 
                    sessionFactory = beanFactory.getBean(SessionFactory.class); 
                
            
        
    

    @Override
    public String onPrepareStatement(String sql) 
        if (mCalcFoundRows.get() == null || !mCalcFoundRows.get().booleanValue()) 
            return sql;
        
        switch (mSQLStatementsPrepared.get()) 

        case 0: 
            mSQLStatementsPrepared.set(mSQLStatementsPrepared.get() + 1);

            // First time, prefix CALC_FOUND_ROWS_HINT

            StringBuilder builder = new StringBuilder(sql);
            int indexOf = builder.indexOf(SELECT_PREFIX);

            if (indexOf == -1) 
                throw new HibernateException("First SQL statement did not contain '" + SELECT_PREFIX + "'");
            

            builder.insert(indexOf + SELECT_PREFIX.length(), CALC_FOUND_ROWS_HINT);
            return builder.toString();
        

        case 1: 
            mSQLStatementsPrepared.set(mSQLStatementsPrepared.get() + 1);

            // Before any secondary selects, capture FOUND_ROWS. If no secondary
            // selects are
            // ever executed, getFoundRows() will capture FOUND_ROWS
            // just-in-time when called
            // directly

            captureFoundRows();
            return sql;
        

        default:
            // Pass-through untouched
            return sql;
        
    

    public void reset() 
        if (mCalcFoundRows.get() != null && mCalcFoundRows.get().booleanValue()) 
            mSQLStatementsPrepared.remove();
            mFoundRows.remove();
            mCalcFoundRows.remove();
        
    

    @Override
    public void afterTransactionCompletion(Transaction tx) 
        reset();
    

    public void setCalcFoundRows(boolean calc) 
        if (calc) 
            mCalcFoundRows.set(Boolean.TRUE);
         else 
            reset();
        
    

    public int getFoundRows() 
        if (mCalcFoundRows.get() == null || !mCalcFoundRows.get().booleanValue()) 
            throw new IllegalStateException("Attempted to getFoundRows without first calling 'setCalcFoundRows'");
        
        if (mFoundRows.get() == null) 
            captureFoundRows();
        

        return mFoundRows.get();
    

    //
    // Private methods
    //

    private void captureFoundRows() 
        init();

        // Sanity checks

        if (mFoundRows.get() != null) 
            throw new HibernateException("'" + SELECT_FOUND_ROWS + "' called more than once");
        

        if (mSQLStatementsPrepared.get() < 1) 
            throw new HibernateException("'" + SELECT_FOUND_ROWS + "' called before '" + SELECT_PREFIX + CALC_FOUND_ROWS_HINT + "'");
        

        // Fetch the total number of rows

        sessionFactory.getCurrentSession().doWork(new Work() 
            @Override
            public void execute(Connection connection) throws SQLException 
                final Statement stmt = connection.createStatement();
                ResultSet rs = null;
                try 
                    rs = stmt.executeQuery(SELECT_FOUND_ROWS);
                    if (rs.next()) 
                        mFoundRows.set(rs.getInt(1));
                     else 
                        mFoundRows.set(0);
                    
                 finally 
                    if (rs != null) 
                        rs.close();
                    
                    try 
                        stmt.close();
                     catch (RuntimeException exp) 

                    
                
            
        );
    

    public void setSessionFactoryBeanName(String sessionFactoryBeanName) 
        this.sessionFactoryBeanName = sessionFactoryBeanName;
    

    @Override
    public void setBeanFactory(BeanFactory arg0) throws BeansException 
        this.beanFactory = arg0;
    


【讨论】:

它是否可以在多线程环境中工作(尤其是在具有多个数据库的环境中,其中两个查询可能最终在不同数据库服务器的不同连接上)?换句话说,是否保证 SQL_CALC_FOUND_ROWS 和 FOUND_ROWS 将在同一个连接中针对同一个数据库主机被调用? 是的。只要您使用与检索结果相同的线程来查询,那么无限连接应该没有问题【参考方案3】:

我找到了一种在休眠中进行分页的方法,而无需对大型数据集进行选择计数 (*)。看看我为我的答案发布的解决方案。

processing a large number of database entries with paging slows down with time

您可以一次执行一个分页,而无需知道您最初需要多少页

【讨论】:

【参考方案4】:

在这个 Hibernate wiki 页面上:

https://www.hibernate.org/314.html

我提出了一个完整的分页解决方案;特别是,元素的总数是通过滚动到结果集的末尾来计算的,现在有几个 JDBC 驱动程序支持这一点。这避免了第二个“计数”查询。

【讨论】:

【参考方案5】:

我认为解决方案取决于您使用的数据库。例如,我们正在使用 MS SQL 并使用下一个查询

select 
  COUNT(Table.Column) OVER() as TotalRowsCount,
  Table.Column,
  Table.Column2
from Table ...

查询的那部分可以用数据库指定的SQL来改变。

我们还设置了我们期望看到的查询最大结果,例如

query.setMaxResults(pageNumber * itemsPerPage)

并获取 ScrollableResults 实例作为查询执行的结果:

ScrollableResults result = null;
try 
    result = query.scroll();
    int totalRowsNumber = result.getInteger(0);
    int from = // calculate the index of row to get for the expected page if any

    /*
     * Reading data form page and using Transformers.ALIAS_TO_ENTITY_MAP
     * to make life easier.
     */ 

finally 
    if (result != null) 
        result.close()

【讨论】:

【参考方案6】:

您可以使用MultiQuery 在单个数据库调用中执行两个查询,这样效率更高。也可以生成count查询,不用每次都写。这是总体思路...

var hql = "from Item where i.Age > :age"
var countHql = "select count(*) " + hql;

IMultiQuery multiQuery = _session.CreateMultiQuery()
    .Add(s.CreateQuery(hql)
            .SetInt32("age", 50).SetFirstResult(10))
    .Add(s.CreateQuery(countHql)
            .SetInt32("age", 50));

var results = multiQuery.List();
var items = (IList<Item>) results[0];
var count = (long)((IList<Item>) results[1])[0];

我想将它包装成一些易于使用的方法会很容易,这样您就可以在一行代码中进行可分页、可计数的查询。

作为替代方法,如果您愿意在nhcontrib 中测试 NHibernate 的工作中 Linq,您可能会发现您可以执行以下操作:

var itemSpec = (from i in Item where i.Age > age);
var count = itemSpec.Count();
var list = itemSpec.Skip(10).Take(10).AsList(); 

显然没有批处理,所以效率不高,但它仍然可以满足您的需求?

希望这会有所帮助!

【讨论】:

我找不到 Hibernate for Java 的等价物(这是 op 需要的)。你知道有没有java版本吗? 我认为 Example 它来自 VB.NET ,但如果它是用于 Java 的,则还有另一个从 Var 转换为 Entity 的过程。这就是我所看到的。【参考方案7】:

MySQLPerformanceBlog.com 的 Baron Schwartz 就此撰写了 post。我希望这个问题有灵丹妙药,但没有。他提出的选项摘要:

    在第一次查询时,获取并缓存所有结果。 不显示所有结果。 不显示总数或到其他页面的中间链接。仅显示“下一个”链接。 估计有多少结果。

【讨论】:

我认为,不获取整个结果,是首先使用分页的原因【参考方案8】:

我知道这个问题并且以前也遇到过。对于初学者来说,执行相同 SELECT 条件的双重查询机制确实不是最佳的。但是,它是有效的,在你开始做一些巨大的改变之前,请意识到它可能不值得。

但是,无论如何:

1) 如果您在客户端处理小数据,请使用结果集实现,该实现可让您将游标设置到集合的末尾,获取其行偏移量,然后将游标重置为先行。

2) 重新设计查询,以便将 COUNT(*) 作为普通行中的额外列。是的,它对每一行都包含相同的值,但它只涉及 1 个额外的整数列。用非聚合值表示聚合值是不恰当的 SQL,但它可能有效。

3) 重新设计查询以使用估计的限制,类似于前面提到的。使用每页的行数和一些上限。例如。只需说“显示 500 个或更多中的 1 到 10 个”之类的话。当他们浏览到“显示 X 的 25o 到 260”时,这是一个稍后的查询,因此您可以通过设置相对于页面 * 行/页面的上限来更新 X 估计值。

【讨论】:

等等,什么?当添加或删除记录时,您要更新表中的每一行 行吗?另外,该选项根本不考虑WHERE 标准。【参考方案9】:

有办法

mysql> SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name
    -> WHERE id > 100 LIMIT 10;
mysql> SELECT FOUND_ROWS();

第二个 SELECT 返回一个数字,表示如果没有 LIMIT 子句,第一个 SELECT 将返回多少行。

参考:FOUND_ROWS()

【讨论】:

这只适用于 MySQL 吗? 不确定不同的数据库,但我会说不 很确定这是 SQL-92 的扩展,而不是跨平台的。以前我错了,第一次遇到这个问题是 6 年前,但我记得出于兼容性原因我不得不避免这种方法。 唯一的缺点是它通常比只执行两个查询具有更大的负担,一个使用 COUNT()。原因是它必须访问所有行,而 COUNT() 通常只需访问索引条目即可满足。 此示例代码的另一个问题是 FOUND_ROWS() 将返回 id > 100 的行数。我不确定这是否是预期的。【参考方案10】:

这是在休眠中完成分页的方式

Query q = sess.createQuery("from DomesticCat cat");
q.setFirstResult(20);
q.setMaxResults(10);
List cats = q.list();

您可以从 hibernate 文档中获取更多信息:http://www.hibernate.org/hib_docs/v3/reference/en-US/html_single/#objectstate-querying-executing-pagination 10.4.1.5 和 10.4.1.6 部分为您提供更多灵活选择。

BR, ~A

【讨论】:

这不是他要问的。看看他发布的代码,你会发现他同时使用了这两种方法。他在问是否有办法避免使用“计数”以及在进行分页时进行选择。【参考方案11】:

如果您不需要显示总页数,那么我不确定您是否需要计数查询。包括谷歌在内的许多网站都不会在分页结果中显示总数。相反,他们只是说“下一个>”。

【讨论】:

同意!我用凯尔建议你的方法解决了这个问题。如果您的经理对此提出质疑,请指出对绩效的影响,然后使用 Google 等真实示例来支持您的主张。 Google 返回结果数量的估计值,它不会向您显示超过前一千个的任何内容,这就是为什么 google 所做的事情是可以接受的 - 这只是一个近似值。在其他情况下,拥有多个页面(包括最后一个页面)的链接通常是一项非常重要的功能。 @Andrew,但是如何在不发出“选择计数(*)”的情况下估算结果数量

以上是关于在 Hibernate 中进行分页是不是有比执行选择和计数查询更有效的方法?的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate分页

Java 中是不是有比 Xalan/Xerces 更快的 XML 解析器 [关闭]

如何使用 Spring Security、Hibernate 和行级 ACL 进行分页

在 Cassandra 的分页选择期间执行更新是不是安全?

SSH后台分页

Hibernate实现分页查询