Есть ли более эффективный способ разбивки на страницы в Hibernate, чем выполнение запросов select и count? - PullRequest
37 голосов
/ 03 октября 2008

Обычно запросы на нумерацию страниц выглядят так. Есть ли лучший способ вместо создания двух почти одинаковых методов, один из которых выполняет "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();
}

Ответы [ 11 ]

11 голосов
/ 08 октября 2008

Барон Шварц на MySQLPerformanceBlog.com создал сообщение об этом. Хотелось бы, чтобы была волшебная пуля для этой проблемы, но ее нет. Краткое описание представленных им вариантов:

  1. При первом запросе извлеките и кэшируйте все результаты.
  2. Не показывать все результаты.
  3. Не показывать общее количество или промежуточные ссылки на другие страницы. Показать только следующую ссылку.
  4. Оцените, сколько результатов.
6 голосов
/ 22 октября 2014

Мое решение будет работать для очень распространенного варианта использования Hibernate + Spring + MySQL

Аналогично приведенному выше ответу, я основал свое решение на 1004 * доктора Ричарда Кеннара. Однако, поскольку Hibernate часто используется в Spring, я хотел, чтобы мое решение работало очень хорошо с Spring и стандартным методом использования Hibernate. Поэтому мое решение использует комбинацию локальных потоков и синглтон-бинов для достижения результата. Технически перехватчик вызывается в каждом подготовленном операторе SQL для SessionFactory, но он пропускает всю логику и не инициализирует какие-либо 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-объект-перехватчик, а затем ссылаться на него в свойстве entityInterceptor объекта SessionFactoryBean. Вы должны установить «sessionFactoryBeanName» только в том случае, если в вашем Spring-контексте имеется более одного SessionFactory и фабрика сеансов, на которую вы хотите сослаться, не называется «sessionFactory». Причина, по которой вы не можете установить ссылку, заключается в том, что это приведет к взаимозависимости между компонентами, которая не может быть разрешена.

Использование 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)» перед выполнением запроса и «reset ()» после [в блоке finally, чтобы убедиться, что он вызывается]:

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

Затем конкретный пример класса DAO для @Entity с именем MyEntity со свойством String "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;
    }

}
5 голосов
/ 03 октября 2008

Если вам не нужно отображать общее количество страниц, тогда я не уверен, что вам нужен запрос на подсчет. На многих сайтах, в том числе в Google, итоговые результаты не отображаются. Вместо этого они просто говорят «следующий>».

3 голосов
/ 27 октября 2008

Вы можете использовать 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];

Я полагаю, было бы достаточно легко обернуть это в какой-нибудь простой в использовании метод, чтобы вы могли иметь разбиваемые на страницы, исчисляемые запросы в одной строке кода.

В качестве альтернативы , если вы хотите проверить работающий Linq для NHibernate в nhcontrib , вы можете обнаружить, что можете сделать что-то подобное:

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

Очевидно, что пакетирование не происходит, так что это не так эффективно, но оно все же может удовлетворить ваши потребности?

Надеюсь, это поможет!

2 голосов
/ 08 октября 2008

Я знаю эту проблему и сталкивался с ней раньше. Для начала, механизм двойного запроса, где он выполняет те же условия SELECT, действительно не оптимален. Но это работает, и прежде чем вы уйдете и сделаете какие-то гигантские изменения, просто поймите, что это может не стоить этого.

Но, в любом случае:

1) Если вы имеете дело с небольшими данными на стороне клиента, используйте реализацию набора результатов, которая позволяет установить курсор в конец набора, получить смещение строки, а затем сбросить курсор до, прежде чем первый.

2) Перепроектируйте запрос, чтобы получить COUNT (*) в качестве дополнительного столбца в обычных строках. Да, он содержит одинаковое значение для каждой строки, но включает только 1 дополнительный столбец, который является целым числом. Это неправильный SQL для представления агрегированных значений с неагрегированными значениями, но он может работать.

3) Перепроектируйте запрос, чтобы использовать предполагаемый лимит, аналогичный тому, который упоминался. Используйте строки на страницу и верхний предел. Например. просто скажите что-то вроде «Показано от 1 до 10 из 500 или более». Когда они переходят к «Отображение 25o-260 of X», это более поздний запрос, так что вы можете просто обновить оценку X, установив верхнюю границу относительно страницы * lines / page.

2 голосов
/ 04 октября 2008

Есть способ

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

Второй SELECT возвращает число, указывающее, сколько строк вернуло бы первый SELECT, если бы он был записан без предложения LIMIT.

Ссылка: FOUND_ROWS ()

1 голос
/ 08 мая 2009

На этой вики-странице Hibernate:

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

Я представляю полное решение для нумерации страниц; в частности, общее количество элементов вычисляется путем прокрутки до конца набора результатов, который поддерживается несколькими драйверами JDBC Это позволяет избежать второго запроса «count».

1 голос
/ 20 апреля 2009

Я думаю, что решение зависит от базы данных, которую вы используете. Например, мы используем 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()
}
0 голосов
/ 17 июня 2013

Вот решение от доктора Ричарда Кеннарда (обратите внимание на исправление ошибки в комментарии блога!) С использованием перехватчиков Hibernate

Для краткости, вы привязываете свой 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();
}
0 голосов
/ 13 сентября 2011

Я нашел способ сделать пейджинг в спящем режиме без использования счетчика выбора (*) для большого размера набора данных. Посмотрите на решение, которое я разместил для моего ответа здесь.

обработка большого количества записей в базе данных с подкачкой со временем замедляется

вы можете выполнять пейджинг по одному, не зная, сколько страниц вам понадобится изначально

...