en

Standardowa implementacja Spring Data JPA wykorzystuje CriteriaBuilder do generowania zapytań typu "Count". Niestety w przypadku Eclipselink zapytania te mają postać:

SELECT COUNT(id) FROM table WHERE [condition]

Okazuje się jednak, że PostgreSQL dla takich zapytań daje znacznie gorsze plany zapytań, nawet pomimo faktu, że "id" jest kluczem głównym tabeli, a więc posiadającym także modyfikator "NOT NULL".

W naszym przypadku dla tabeli liczącej ponad 1,8mln rekordów i prostego warunku, do którego pasuje niestety większość rekordów plan takiego zapytania to niestety Full Scan bez wykorzystania indeksu pasującego do tego warunku. Wynika to najwyraźniej z faktu, że PostgreSQL odwołuje się do wartości w kolumnie "id" testując je pod kątem wartości pustych (NULL). Wykonywane analizy "explain" wskazują w tym wypadku koszt ~713k w porównaniu do ~90k dla zapytani SELECT COUNT(*). Dość znaczna różnica nad którą warto się pochylić.

Dalsza analiza wykazała, że w naszym wypadku zapytanie SELECT COUNT(1) ma identyczny plan co SELECT COUNT(*), co ma istotne znaczenie dla przyjętego rozwiązania.

Rozwiązanie

Ponieważ ingerencja w Eclipselink okazała się zbyt skomplikowana postanowiliśmy zmodyfikować działanie Spring Data JPA. W tym wypadku oznacza to dostarczenie własnej implementacji bazowej repozytoriów, która inaczej generuje zapytania o ilość rekordów.

JpaRepositoryImpl


public class JpaRepositoryImpl<T, ID extends Serializable>
    extends SimpleJpaRepository<T, ID> {

    private final EntityManager em;

    public JpaRepositoryImpl(JpaEntityInformation<T, ?> entityInformation,
        EntityManager em) {
        super(entityInformation, em);
        this.em = em;
    }

    protected List<Expression<?>> buildList(Expression<?>... expressions) {
        ArrayList list = new ArrayList();
        for (Expression<?> exp : expressions) {
            list.add(exp);
        }
        return list;
    }

    protected <T> Expression<T> internalLiteral(Metamodel metamodel, T value) {
        return new ExpressionImpl<T>(metamodel,
            (Class<T>) (value == null ? null : value.getClass()),
            new ConstantExpression(value, new ExpressionBuilder()), value);
    }

    @Override
    protected TypedQuery<Long> getCountQuery(Specification<T> spec) {

        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);

        Root<T> root = applySpecificationToCriteria(spec, query);

        if (query.isDistinct()) {
            query.select(builder.countDistinct(root));
        } else {
            org.eclipse.persistence.expressions.Expression node =
                new ConstantExpression(new Long(1), new ExpressionBuilder());

            FunctionExpressionImpl cnt = new FunctionExpressionImpl(
                em.getMetamodel(),
                ClassConstants.LONG,
                node.count(),
                buildList(internalLiteral(em.getMetamodel(), new Long(1))),
                "COUNT");
            query.select(cnt);
        }

        return em.createQuery(query);
    }

    private <S> Root<T> applySpecificationToCriteria(Specification<T> spec,
        CriteriaQuery<S> query) {

        Root<T> root = query.from(getDomainClass());

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }
}

powyższa implementacja zawiera jedną kluczową zmianę: metoda "getCountQuery" buduje zapytanie nie w oparciu o

builder.count(root);

a nieco bardziej bardziej skomplikowany kod generujący wyrażenie COUNT(1)

CustomRepositoryFactoryBean

Aby nasza klasa bazowa była prwidłowo wykorzystywana przez Spring Data należy dostarczyć jeszcze fabrykę, która będzie zwracać instancje naszej klasy:


public class CustomRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable>
    extends JpaRepositoryFactoryBean<R, T, I> {

    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {

        return new MyRepositoryFactory(entityManager);
    }

    private static class MyRepositoryFactory<T, I extends Serializable>
        extends JpaRepositoryFactory {

        private EntityManager entityManager;

        public MyRepositoryFactory(EntityManager entityManager) {
            super(entityManager);

            this.entityManager = entityManager;
        }

        protected Object getTargetRepository(RepositoryMetadata metadata) {
            JpaEntityInformation<?, Serializable> entityInformation =
                getEntityInformation(metadata.getDomainType());
            return new JpaRepositoryImpl<T, I>(
                (JpaEntityInformation<T, ?>) entityInformation,
                entityManager);
        }

        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
            return JpaRepositoryImpl.class;
        }
    }
}

Konfiguracja

Na sam koniec pozostaje wskazanie fabryki w konfiguracji mechanizmu tworzącego beany repozytoriów. W naszym wypadku używamy konfiguracji XML, więc ma ona postać:



<jpa:repositories
    base-package="app.dao"
    factory-class="app.jpa.CustomRepositoryFactoryBean"/>

 

W efekcie uzyskujemy zapytania w postaci SELECT COUNT(1) dla wszystkich mechanizmów Spring Data, w tym (co dla nas było najistotniejsze) dla zapytań stronicowanych zwracających obiekt Page.

Autor: Maciej Liżewski, 3e Software House

Zamknij ten komunikat

Nasze strony wykorzystują pliki cookies.

Na naszych stronach używamy informacji zapisanych za pomocą cookies m.in. w celach reklamowych i statystycznych. Mogą też stosować je współpracujące z nami podmioty, takie jak firmy badawcze oraz dostawcy aplikacji multimedialnych. W każdej przeglądarce internetowej można zmienić ustawienia dotyczące cookies. Korzystanie z naszych serwisów internetowych bez zmiany ustawień dotyczących cookies oznacza, że będą one zapisane w pamięci urządzenia.