web-dev-qa-db-fra.com

Existe-t-il un moyen plus efficace de paginer dans Hibernate que d’exécuter des requêtes select et count?

Les requêtes de pagination ressemblent généralement à ceci. Existe-t-il un meilleur moyen de créer deux méthodes presque égales, l'une exécutant "select * ..." et l'autre "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();
}
37
serg

Baron Schwartz de MySQLPerformanceBlog.com a écrit un post à ce sujet. J'aimerais qu'il y ait une solution miracle à ce problème, mais ce n'est pas le cas. Résumé des options qu'il a présentées:

  1. Sur la première requête, récupérez et mettez en cache tous les résultats.
  2. Ne pas afficher tous les résultats.
  3. Ne pas afficher le nombre total ou les liens intermédiaires vers d'autres pages. Afficher uniquement le lien "suivant".
  4. Estimez combien de résultats il y a.
12
Eric R. Rath

Ma solution fonctionnera pour le cas d'utilisation très courant d'Hibernate + Spring + MySQL

Semblable à la réponse ci-dessus, j'ai basé ma solution sur du Dr Richard Kennar . Cependant, comme Hibernate est souvent utilisé avec Spring, je souhaitais que ma solution fonctionne très bien avec Spring et la méthode standard d’utilisation d’Hibernate. Par conséquent, ma solution utilise une combinaison de locals de threads et de beans singleton pour obtenir le résultat. Techniquement, l'intercepteur est appelé sur chaque instruction SQL préparée pour la SessionFactory, mais il ignore toute la logique et n'initialise aucun ThreadLocal (s) sauf s'il s'agit d'une requête spécifiquement définie pour compter le nombre total de lignes.

En utilisant la classe ci-dessous, votre configuration Spring ressemble à ceci:

<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"/>

Fondamentalement, vous devez déclarer le bean interceptor, puis le référencer dans la propriété "entityInterceptor" de SessionFactoryBean. Vous ne devez définir "sessionFactoryBeanName" que s'il existe plusieurs SessionFactory dans votre contexte Spring et que la fabrique de session que vous souhaitez référencer ne s'appelle pas "sessionFactory". La raison pour laquelle vous ne pouvez pas définir de référence est que cela provoquerait une interdépendance entre les beans qui ne peut pas être résolue.

Utiliser un haricot wrapper pour le résultat:

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

Ensuite, en utilisant une classe DAO de base abstraite, vous devez appeler "setCalcFoundRows (true)" avant de lancer la requête et "reset ()" après [dans un bloc finally pour s'assurer qu'elle s'appelle]:

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

Ensuite, un exemple concret de classe DAO pour un @Entity nommé MyEntity avec une propriété 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);
    }
}

Enfin la classe intercepteur qui fait tout le travail:

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

}
6
Yinzara

Si vous n'avez pas besoin d'afficher le nombre total de pages, je ne suis pas sûr que vous ayez besoin de la requête de nombre. Beaucoup de sites, y compris Google, n’affiche pas le total des résultats paginés. Au lieu de cela, ils disent simplement "suivant>".

5
Kyle Dyer

Vous pouvez utiliser MultiQuery pour exécuter les deux requêtes dans un seul appel de base de données, ce qui est beaucoup plus efficace. Vous pouvez également générer la requête de comptage afin que vous n'ayez pas à l'écrire à chaque fois. Voici l'idée générale ...

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

J'imagine qu'il serait assez facile de résumer cela dans une méthode facile à utiliser afin que vous puissiez avoir des requêtes paginables et dénombrables dans une seule ligne de code.

En tant que alternative , si vous êtes prêt à tester le travail en cours Linq pour NHibernate dans nhcontrib , vous constaterez peut-être que vous pouvez faire quelque chose comme ceci:

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

De toute évidence, il n’ya pas de traitement par lots, ce n’est donc pas aussi efficace, mais peut-il répondre à vos besoins?

J'espère que cela t'aides!

3
tobinharris

Il y a un moyen

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

Le second SELECT renvoie un nombre indiquant le nombre de lignes que le premier SELECT aurait renvoyé s'il avait été écrit sans la clause LIMIT.

Référence: FOUND_ROWS ()

2
michal kralik

Je connais ce problème et y ai déjà fait face. Pour commencer, le mécanisme de double requête qui applique les mêmes conditions SELECT n’est en effet pas optimal. Mais cela fonctionne, et avant de vous lancer dans un changement géant, sachez que cela n'en vaut peut-être pas la peine.

Mais, de toute façon:

1) Si vous travaillez avec de petites données côté client, utilisez une implémentation d'ensemble de résultats qui vous permet de définir le curseur à la fin de l'ensemble, d'obtenir son décalage de ligne, puis de réinitialiser le curseur sur avant.

2) Modifiez la requête pour obtenir COUNT (*) en tant que colonne supplémentaire dans les lignes normales. Oui, il contient la même valeur pour chaque ligne, mais il ne s'agit que d'une colonne supplémentaire qui est un entier. Ce n'est pas un SQL correct de représenter une valeur agrégée avec des valeurs non agrégées, mais cela peut fonctionner.

3) Redéfinissez la requête pour utiliser une limite estimée, similaire à celle mentionnée. Utilisez des lignes par page et une limite supérieure. Par exemple. il suffit de dire quelque chose comme "Affichage de 1 à 10 sur 500 ou plus". Lorsqu'ils accèdent à "Affichage de 25o à 260 sur X", la requête est postérieure. Vous pouvez donc mettre à jour l'estimation de X en faisant la limite supérieure relative à page * rows/page.

2
Josh

Je pense que la solution dépend de la base de données que vous utilisez. Par exemple, nous utilisons MS SQL et la requête suivante

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

Cette partie de la requête peut être modifiée avec la base de données spécifiée SQL.

Nous définissons également le résultat de la requête max que nous attendons, par exemple.

query.setMaxResults(pageNumber * itemsPerPage)

Et obtient l'instance ScrollableResults comme résultat de l'exécution de la requête:

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()
}
1
ruslan

Sur cette page wiki Hibernate:

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

Je présente une solution complète de pagination; en particulier, le nombre total d'éléments est calculé en faisant défiler l'écran jusqu'à la fin du jeu de résultats, pris en charge par plusieurs pilotes JDBC. Cela évite la seconde requête "count".

1

Voici une solution par le Dr Richard Kennard (attention au correctif dans le commentaire du blog!), En utilisant Hibernate Interceptors

Pour résumer, vous liez votre sessionFactory à votre classe interceptor afin que votre intercepteur puisse vous donner le nombre de lignes trouvées plus tard. 

Vous pouvez trouver le code sur le lien de la solution. Et ci-dessous est un exemple d'utilisation.

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
kommradHomer

J'ai trouvé un moyen de faire de la pagination en veille prolongée sans faire un nombre de sélections (*) sur une taille de jeu de données importante. Regardez la solution que j'ai postée pour ma réponse ici.

le traitement d'un grand nombre d'entrées de la base de données avec la pagination ralentit avec le temps

vous pouvez effectuer une pagination à la fois sans savoir combien de pages vous aurez besoin à l’origine

0
randomThought