39

通常、ページネーション クエリは次のようになります。2 つのほぼ同等のメソッドを作成する代わりに、より良い方法はありますか? そのうちの 1 つは「select *...」を実行し、もう 1 つは「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();
}
4

11 に答える 11

10

MySQLPerformanceBlog.com の Baron Schwartz が、これに関する投稿を作成しました。この問題に対する特効薬があればいいのにと思いますが、そうではありません。彼が提示したオプションの要約:

  1. 最初のクエリで、すべての結果をフェッチしてキャッシュします。
  2. すべての結果を表示しないでください。
  3. 合計数や他のページへの中間リンクを表示しないでください。「次へ」のリンクのみを表示します。
  4. 結果がいくつあるかを見積もります。
于 2008-10-08T17:30:18.407 に答える
8

私のソリューションは、Hibernate + Spring + MySQL の非常に一般的なユースケースで機能します

上記の回答と同様に、リチャード・ケナー博士の. ただし、Hibernate は Spring でよく使用されるため、Spring と Hibernate を使用する標準的な方法でうまく機能するソリューションが必要でした。したがって、私のソリューションでは、スレッド ローカルとシングルトン Bean の組み合わせを使用して結果を達成します。技術的には、SessionFactory に対して準備されたすべての SQL ステートメントでインターセプターが呼び出されますが、合計行をカウントするように特別に設定されたクエリでない限り、すべてのロジックをスキップし、ThreadLocal を初期化しません。

以下のクラスを使用すると、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();
        }
    }
}

次に、文字列プロパティ「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;
    }

}
于 2014-10-22T01:34:06.240 に答える
5

合計ページ数を表示する必要がない場合は、カウントクエリが必要かどうかわかりません。グーグルを含む多くのサイトは、ページングされた結果に合計を表示しません。代わりに、彼らは単に「次へ>」と言います。

于 2008-10-03T18:56:33.337 に答える
3

MultiQueryを使用すると、1 回のデータベース呼び出しで両方のクエリを実行できます。これははるかに効率的です。カウント クエリを生成することもできるため、毎回記述する必要はありません。一般的な考え方は次のとおりです...

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

これを使いやすいメソッドにまとめて、1 行のコードでページ分割可能でカウント可能なクエリを作成できるようにするのは簡単だと思います。

の方法として、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(); 

明らかにバッチ処理が行われていないため、それほど効率的ではありませんが、それでもニーズに合っているでしょうか?

お役に立てれば!

于 2008-10-27T14:10:47.237 に答える
2

私はこの問題を知っており、以前に直面したことがあります。まず、同じ SELECT 条件を実行する二重クエリ メカニズムは最適ではありません。しかし、それはうまくいきます。大きな変化を起こす前に、その価値がないかもしれないことを理解してください.

しかし、とにかく:

1)クライアント側で小さなデータを扱っている場合は、結果セットの実装を使用して、カーソルをセットの最後に設定し、その行オフセットを取得してから、最初にカーソルを前にリセットします。

2) 通常の行の余分な列として COUNT(*) を取得するように、クエリを再設計します。はい、すべての行に同じ値が含まれていますが、整数である余分な列が 1 つだけ含まれています。これは、集計された値を集計されていない値で表すのに不適切な SQL ですが、機能する可能性があります。

3) 前述のように、推定制限を使用するようにクエリを再設計します。ページあたりの行といくつかの上限を使用します。たとえば、「500 以上の 1 から 10 を表示しています」のように言うだけです。彼らが「Showing 25o to 260 of X」をブラウズすると、それは後のクエリであるため、ページ * 行/ページに相対的な上限を作成することで X の見積もりを更新できます。

于 2008-10-08T13:56:59.697 に答える
2

やり方がある

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

2 番目の SELECT は、最初の SELECT が LIMIT 句なしで記述されていた場合に返される行数を示す数値を返します。

参考:FOUND_ROWS()

于 2008-10-03T23:30:26.500 に答える
1

At this Hibernate wiki page:

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

I present a complete pagination solution; in particular, the total number of elements is computed by scrolling to the end of the resultset, which is supported by now by several JDBC drivers. This avoids the second "count" query.

于 2009-05-07T20:17:11.243 に答える
1

解決策は、使用しているデータベースに依存すると思います。たとえば、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()
}
于 2009-04-20T09:47:20.070 に答える
0

大規模なデータセット サイズで選択カウント (*) を実行せずに、休止状態でページングを行う方法を見つけました。ここに私の答えとして投稿した解決策を見てください。

ページングによる多数のデータベース エントリの処理は、時間の経過とともに遅くなります。

元々必要なページ数を知らなくても、一度に 1 つずつページングを実行できます。

于 2011-09-13T16:44:52.180 に答える
-5

これは、休止状態でページネーションが行われる方法です

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 10.4.1.5 および 10.4.1.6 セクションの hibernate ドキュメントから詳細情報を取得できます。より柔軟なオプション。

BR、
~A

于 2008-10-03T20:54:16.097 に答える