it-swarm.com.de

Gibt es eine effizientere Methode, um im Ruhezustand eine Paginierung vorzunehmen, als Abfragen zum Auswählen und Zählen auszuführen?

Normalerweise sehen Paginierungsabfragen so aus. Gibt es einen besseren Weg, als zwei fast gleiche Methoden zu erstellen, von denen eine "select * ..." und die andere "count * ..." ausführt?

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 von MySQLPerformanceBlog.com hat dazu ein post verfasst. Ich wünschte, es gäbe eine magische Kugel für dieses Problem, aber das gibt es nicht. Zusammenfassung der Optionen, die er präsentierte:

  1. Rufen Sie bei der ersten Abfrage alle Ergebnisse ab und zwischenspeichern Sie sie.
  2. Nicht alle Ergebnisse anzeigen.
  3. Zeigen Sie nicht die Gesamtzahl oder die Zwischenlinks zu anderen Seiten an. Zeige nur den "nächsten" Link.
  4. Schätzen Sie, wie viele Ergebnisse es gibt.
12
Eric R. Rath

Meine Lösung funktioniert für den sehr häufigen Anwendungsfall von Hibernate + Spring + MySQL

Ähnlich wie in der obigen Antwort stützte ich mich auf Dr. Richard Kennar's . Da Hibernate jedoch häufig mit Spring verwendet wird, wollte ich, dass meine Lösung sehr gut mit Spring und der Standardmethode für Hibernate funktioniert. Daher verwendet meine Lösung eine Kombination aus lokalen Threads und Singleton-Beans, um das Ergebnis zu erzielen. Technisch wird der Interceptor in jeder vorbereiteten SQL-Anweisung für die SessionFactory aufgerufen, überspringt jedoch die gesamte Logik und initialisiert keine ThreadLocal (s), es sei denn, es handelt sich um eine Abfrage, die speziell zum Zählen der Gesamtzeilen festgelegt ist.

Mit der folgenden Klasse sieht Ihre Spring-Konfiguration folgendermaßen aus:

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

Grundsätzlich müssen Sie die Interceptor-Bean deklarieren und anschließend in der Eigenschaft "entityInterceptor" der SessionFactoryBean referenzieren. Sie müssen "sessionFactoryBeanName" nur festlegen, wenn in Ihrem Spring-Kontext mehr als eine SessionFactory vorhanden ist und die Session-Factory, auf die Sie verweisen möchten, nicht "sessionFactory" heißt. Der Grund, warum Sie keinen Verweis setzen können, liegt darin, dass dies zu einer Interdependenz zwischen den Beans führen würde, die nicht aufgelöst werden kann.

Verwenden einer Wrapper-Bean für das Ergebnis:

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

Dann müssen Sie mit einer abstrakten DAO-Basisklasse "setCalcFoundRows (true)" aufrufen, bevor Sie die Abfrage und "reset ()" nach [in einem finally-Block ausführen, um sicherzustellen, dass sie aufgerufen wird]:

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

Dann ein konkretes DAO-Klassenbeispiel für eine @Entity namens MyEntity mit einer String-Eigenschaft "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);
    }
}

Schließlich die Interceptor-Klasse, die die ganze Arbeit erledigt:

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

Wenn Sie nicht die Gesamtzahl der Seiten anzeigen müssen, bin ich nicht sicher, ob Sie die Zählabfrage benötigen. Viele Websites, einschließlich Google, zeigen nicht die Gesamtsumme der Seitenergebnisse. Stattdessen sagen sie einfach "weiter>".

5
Kyle Dyer

Sie können MultiQuery verwenden, um beide Abfragen in einem einzigen Datenbankaufruf auszuführen, was wesentlich effizienter ist. Sie können auch die Zählabfrage generieren, so dass Sie sie nicht jedes Mal schreiben müssen. Hier ist die allgemeine Idee ...

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

Ich kann mir vorstellen, dass es leicht genug wäre, dies in einer einfach zu verwendenden Methode zusammenzufassen, sodass Sie paginierbare, zählbare Abfragen in einer einzigen Codezeile haben können.

Als Alternative , wenn Sie bereit sind, den in Arbeit befindlichen Linq für NHibernate in nhcontrib zu testen, stellen Sie möglicherweise Folgendes fest:

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

Offensichtlich läuft kein Batching ab, also ist das nicht so effizient, aber es kann trotzdem Ihren Bedürfnissen entsprechen?

Hoffe das hilft!

3
tobinharris

Da ist ein Weg

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

Das zweite SELECT gibt eine Zahl zurück, die angibt, wie viele Zeilen das erste SELECT zurückgegeben hätte, wenn es ohne die LIMIT-Klausel geschrieben worden wäre.

Referenz: FOUND_ROWS ()

2
michal kralik

Ich kenne dieses Problem und habe es schon früher gesehen. Für Anfänger ist der Doppelabfragemechanismus, bei dem dieselben SELECT-Bedingungen ausgeführt werden, in der Tat nicht optimal. Aber es funktioniert, und bevor Sie losgehen und ein paar große Veränderungen vornehmen, müssen Sie sich klar machen, dass es das nicht wert ist.

Wie auch immer:

1) Wenn Sie auf der Clientseite mit kleinen Daten zu tun haben, verwenden Sie eine Ergebnismengenimplementierung, mit der Sie den Cursor an das Ende der Gruppe setzen können, den Zeilenversatz abrufen und dann den Cursor auf vorher zurücksetzen.

2) Gestalten Sie die Abfrage neu, sodass Sie COUNT (*) als zusätzliche Spalte in den normalen Zeilen erhalten. Ja, es enthält für jede Zeile den gleichen Wert, aber nur eine zusätzliche Spalte, die eine ganze Zahl ist. Dies ist nicht korrektes SQL, um einen aggregierten Wert mit nicht aggregierten Werten darzustellen, es kann jedoch funktionieren.

3) Gestalten Sie die Abfrage neu, um einen geschätzten Grenzwert zu verwenden, ähnlich wie erwähnt. Verwenden Sie Zeilen pro Seite und einige Obergrenzen. Z.B. Sagen Sie einfach etwas wie "Zeige 1 bis 10 von 500 oder mehr". Wenn sie zu "Showing 25o to 260 of X" navigieren, ist dies eine spätere Abfrage, sodass Sie die X-Schätzung einfach aktualisieren können, indem Sie die obere Schranke relativ zu page * Zeilen/Seite festlegen.

2
Josh

Ich denke, die Lösung hängt von der verwendeten Datenbank ab. Beispielsweise verwenden wir MS SQL und verwenden die nächste Abfrage

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

Dieser Teil der Abfrage kann mit datenbankspezifischem SQL geändert werden.

Außerdem legen wir das maximale Abfrageergebnis fest, das wir erwarten, z.

query.setMaxResults(pageNumber * itemsPerPage)

Ruft die ScrollableResults-Instanz als Ergebnis der Abfrageausführung ab:

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

Auf dieser Hibernate-Wiki-Seite:

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

Ich präsentiere eine vollständige Paginierungslösung. Die Gesamtzahl der Elemente wird insbesondere durch Scrollen bis zum Ende des Resultsets berechnet, das inzwischen von mehreren JDBC-Treibern unterstützt wird. Dies vermeidet die zweite Abfrage "count".

1

Hier ist eine Lösung von Dr. Richard Kennard (Beachte den Bug-Fix im Blog-Kommentar!) Mit Hibernate Interceptors

Zur Zusammenfassung binden Sie Ihre sessionFactory an Ihre Interceptor-Klasse, sodass Ihnen Ihr Interceptor später die Anzahl der gefundenen Zeilen angeben kann. 

Sie finden den Code auf dem Lösungslink. Und unten ist eine Beispielanwendung.

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

Ich habe einen Weg gefunden, Paging im Hibernate durchzuführen, ohne einen select count (*) über eine große Dataset-Größe durchzuführen. Sehen Sie sich hier die Lösung an, die ich für meine Antwort gepostet habe.

die Verarbeitung einer großen Anzahl von Datenbankeinträgen mit Auslagern verlangsamt sich mit der Zeit

sie können die Seiten einzeln aufrufen, ohne zu wissen, wie viele Seiten Sie ursprünglich benötigen

0
randomThought