在执行select和count查询的时候,有没有比Hibernate更高效的分页方式?

通常分页查询看起来像这样。 有没有更好的方法,而不是使两个几乎相等的方法,其中之一执行“select* …”,另一个“计数* …”?

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(); } 

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

  1. 在第一个查询中,获取并caching所有结果。
  2. 不要显示所有结果。
  3. 不要显示总数或其他页面的中间链接。 只显示“下一个”链接。
  4. 估计有多less结果。

我的解决scheme将适用于Hibernate + Spring + MySQL的常见用例

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

使用下面的类,你的Springconfiguration如下所示:

 <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”。 您无法设置引用的原因是,这将导致无法parsing的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(); } } } 

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

 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; } } 

如果你不需要显示总页数,我不知道你需要计数查询。 包括谷歌在内的许多网站都没有显示分页结果的总数。 相反,他们只是说“下一个”。

您可以使用MultiQuery在单个数据库调用中执行这两个查询,效率更高。 你也可以生成计数查询,所以你不必每次都写。 这是一般的想法…

 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中testing正在运行的NHibernate的Linq,你可能会发现你可以这样做:

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

很明显,没有批量生产,所以效率不高,但仍然可以满足您的需求?

希望这可以帮助!

有一种方法

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

第二个SELECT返回一个数字,指示在没有LIMIT子句的情况下,第一个SELECT将返回多less行。

参考: FOUND_ROWS()

我知道这个问题,并已经面对过它。 对于初学者来说,双击查询机制,它做相同的SELECT条件确实不是最佳的。 但是,它起作用了,在你离开之前做一些巨大的改变,只是意识到这可能不值得。

但是,无论如何:

1)如果你在处理客户端的小数据,使用一个结果集的实现,它可以让你把游标设置到集合的末尾,得到它的行偏移量,然后把游标重置到最先。

2)重新devise查询,以便将COUNT(*)作为普通行中的额外列。 是的,它包含每行相同的值,但它只涉及1个额外的列是一个整数。 这是不正确的SQL来表示非聚合值的聚合值,但它可能工作。

3)重新devise查询使用估计的限制,类似于提到的。 每页使用行数和一些上限。 例如,只要说出“显示500或更多的1到10”之类的东西。 当他们浏览到“显示X的25o到260”时,它的后面的查询,所以你可以通过使上限相对于页*行/页来更新X估计值。

我认为解决scheme取决于你正在使用的数据库。 例如,我们正在使用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() } 

在这个Hibernate wiki页面上:

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

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

我find了一种在hibernate中进行分页的方法,而不用在大数据集大小上进行select count(*)。 看看我在这里发布的答案。

处理大量的数据库条目随着时间的推移而变慢

您可以一次执行一个分页,而不必知道最初需要多less页

Richard Kennard博士(使用Hibernate Interceptors介绍博客评论中的错误修复)提供了一个解决scheme

总结一下,你将sessionFactory绑定到你的拦截器类,这样你的拦截器可以给你稍后发现的行数。

您可以在解决scheme链接中find代码。 下面是一个示例用法。

 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(); } 

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

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

你可以从http://www.hibernate.org/hib_docs/v3/reference/en-US/html_single/#objectstate-querying-executing-pagination获得更多的hibernate文档信息。10.4.1.5和10.4.1.6节给你更多的flexbile选项。;

BR,
〜一