programing

최대 절전 모드에서 선택 및 카운트 쿼리를 실행하는 것보다 더 효율적으로 페이지를 작성하는 방법이 있습니까?

oldcodes 2023. 8. 12. 10:39
반응형

최대 절전 모드에서 선택 및 카운트 쿼리를 실행하는 것보다 더 효율적으로 페이지를 작성하는 방법이 있습니까?

일반적으로 페이지화 쿼리는 다음과 같습니다.하나는 "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();
}

MySQLPerformanceBlog.com 의 슈워츠 남작은 이에 대한 게시물을 작성했습니다.이 문제에 대한 마법의 총알이 있으면 좋겠지만, 없습니다.그가 제시한 옵션의 요약:

  1. 첫 번째 쿼리에서 모든 결과를 가져오고 캐시합니다.
  2. 모든 결과를 표시하지 않습니다.
  3. 총 개수 또는 다른 페이지에 대한 중간 링크를 표시하지 않습니다.다음 링크만 표시합니다.
  4. 결과가 몇 개인지 추정합니다.

내 솔루션은 Hibernate+Spring+MySQL의 매우 일반적인 사용 사례에 적합합니다.

위의 답변과 마찬가지로, 저는 Richard Kennar 박사의 솔루션을 기반으로 했습니다.그러나 Hibernate는 Spring과 함께 사용되는 경우가 많기 때문에 Spring과 Hibernate를 사용하는 표준 방법과 함께 사용할 수 있는 솔루션을 원했습니다.따라서 저의 솔루션은 결과를 얻기 위해 스레드 로컬과 싱글톤 콩의 조합을 사용합니다.기술적으로 인터셉터는 SessionFactory에 대해 준비된 모든 SQL 문에서 호출되지만 전체 행을 카운트하도록 특별히 설정된 쿼리가 아니면 모든 논리를 건너뛰고 스레드로컬을 초기화하지 않습니다.

아래 클래스를 사용하여 스프링 구성은 다음과 같습니다.

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

기본적으로 인터셉트를 빈으로 선언한 다음 "엔티티"에서 참조해야 합니다.SessionFactoryBean의 Interceptor" 속성입니다.Spring 컨텍스트에 둘 이상의 SessionFactory가 있고 참조하려는 세션 팩토리가 "sessionFactory"가 아닌 경우에만 "sessionFactoryBeanName"을 설정해야 합니다.기준을 설정할 수 없는 이유는 콩 사이의 상호 의존성을 해결할 수 없기 때문입니다.

결과에 래퍼 빈 사용:

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()"을 호출해야 합니다.

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

}

총 페이지 수를 표시할 필요가 없다면 카운트 쿼리가 필요한지 모르겠습니다.구글을 포함한 많은 사이트들은 페이지 결과에 총계를 표시하지 않습니다.대신 그들은 "다음"이라고만 말합니다.

다중 쿼리를 사용하여 단일 데이터베이스 호출에서 두 쿼리를 모두 실행할 수 있으므로 훨씬 효율적입니다.카운트 쿼리를 생성할 수도 있으므로 매번 작성할 필요가 없습니다.여기 일반적인 생각이 있습니다...

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

이것을 사용하기 쉬운 방법으로 마무리하는 것이 충분히 쉬울 것이라고 생각합니다. 그래서 당신은 코드 한 줄에 페이지로 묶을 수 있고, 셀 수 있는 쿼리를 가질 수 있습니다.

대신 진행 중인 Linkq for NHibernate in nhcontrib을 테스트할 의향이 있다면 다음과 같은 작업을 수행할 수 있습니다.

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가 반환한 행 수를 나타내는 숫자를 반환합니다.

참조: FOUND_ROWS()

저는 이 문제를 알고 있고 이전에 직면한 적이 있습니다.우선, 동일한 SELECT 조건을 수행하는 이중 쿼리 메커니즘은 실제로 최적이 아닙니다.하지만, 그것은 효과가 있습니다. 그리고 여러분이 큰 변화를 하기 전에, 단지 그것이 가치가 없을 수도 있다는 것을 깨달으세요.

하지만 어쨌든:

클라이언트 측에서 작은 데이터를 처리하는 경우, 커서를 세트의 끝으로 설정하고 행 오프셋을 가져온 다음 커서를 먼저 이전으로 재설정할 수 있는 결과 세트 구현을 사용합니다.

일반 행에 COUNT(*) 열이 추가로 표시되도록 쿼리를 다시 설계합니다.예, 모든 행에 대해 동일한 값이 포함되지만 정수인 추가 열은 하나만 포함됩니다.집계되지 않은 값으로 집계된 값을 나타내기에는 부적절한 SQL이지만 작동할 수 있습니다.

언급된 것과 유사하게 추정된 제한을 사용하도록 쿼리를 다시 설계합니다.페이지당 행 수와 일부 상한을 사용합니다.예를 들어 "500개 중 1개에서 10개 이상 표시"와 같은 말을 합니다.이들이 "25o ~ 260 of X 표시"를 검색하면 나중에 쿼리가 되므로 페이지 * 행/페이지를 기준으로 상한을 설정하여 X 추정치를 업데이트할 수 있습니다.

당신이 사용하는 데이터베이스에 따라 해결책이 다르다고 생각합니다.예를 들어 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()
}

최대 절전 모드 Wiki 페이지에서:

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

완전한 페이지화 솔루션을 제시합니다. 특히, 여러 JDBC 드라이버가 지원하는 결과 집합의 끝까지 스크롤하여 총 요소 수를 계산합니다.이렇게 하면 두 번째 "카운트" 쿼리가 방지됩니다.

큰 데이터 세트 크기에 대해 선택 카운트(*)를 수행하지 않고 최대 절전 모드에서 페이징을 수행하는 방법을 찾았습니다.제가 여기에 제 답변을 위해 올린 솔루션을 보세요.

페이징을 사용하여 많은 수의 데이터베이스 항목을 처리하면 시간이 지남에 따라 속도가 느려집니다.

원래 필요한 페이지 수를 알지 못하고 한 번에 하나씩 페이징을 수행할 수 있습니다.

다음은 최대 절전 모드 인터셉트를 사용한 리처드 케너드 박사의 해결책입니다(블로그 댓글의 버그 수정에 주의하세요!).

요약을 위해 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();
}

동면 상태에서 페이지화가 수행되는 방법은 다음과 같습니다.

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-query-query-discination 10.4.1.5 및 10.4.1.6 섹션은 더 많은 유연한 파일 옵션을 제공합니다.

BR,
~A

언급URL : https://stackoverflow.com/questions/168084/is-there-a-more-efficient-way-of-making-pagination-in-hibernate-than-executing-s

반응형