Рубрики
Без рубрики

Руководство для начинающих по кэшированию JPQL в режиме гибернации и собственного плана запросов

Узнайте, как кэш плана запросов Hibernate может помочь вам сократить время отклика, избегая повторного анализа JPQL, API критериев или собственных SQL-запросов.

Автор оригинала: Vlad Mihalcea.

Каждый запрос JPQL должен быть скомпилирован перед выполнением, и, поскольку этот процесс может быть ресурсоемким, Hibernate предоставляет QueryPlanCache для этой цели.

Для запросов сущностей представление запроса String анализируется в AST (Абстрактное синтаксическое дерево). Для собственных запросов этап синтаксического анализа не может скомпилировать запрос, поэтому он извлекает только информацию об именованных параметрах и типе возвращаемого запроса.

Руководство для начинающих по JPQL в режиме гибернации и встроенному хранилищу запросов @vlad_mihalcea https://t.co/9vf3a4Ty5V pic.twitter.com/mhTDFM9Ifr

Кэш плана запросов является общим как для сущностных, так и для собственных запросов, и его размер регулируется следующим свойством конфигурации:


По умолчанию QueryPlanCache хранит 2048 планов, которых может быть недостаточно для крупных корпоративных приложений.

Для собственных запросов QueryPlanCache хранит также ParameterMetadata , в котором содержится информация об имени параметра, положении и соответствующем типе гибернации. Кэш ParameterMetadata управляется с помощью следующего свойства конфигурации:


Если приложение выполняет больше запросов, чем может вместить QueryPlanCache , из – за компиляции запроса произойдет снижение производительности.

Предполагая, что в нашем приложении есть следующие объекты:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @OneToMany(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List comments = new ArrayList<>();

    public void addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
    }
    
    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

Мы собираемся измерить фазу компиляции для следующих JPQL и собственных запросов:

protected Query getEntityQuery1(EntityManager entityManager) {
    return entityManager.createQuery("""
        select new
           com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary(
               p.id, p.title, c.review 
		   )
        from PostComment c
        join c.post p
		""")
    .setFirstResult(10)
    .setMaxResults(20)
    .setHint(QueryHints.HINT_FETCH_SIZE, 20);
}

protected Query getEntityQuery2(EntityManager entityManager) {
    return entityManager.createQuery("""
        select c
        from PostComment c
        join fetch c.post p
        where p.title like :title
		"""
    );
}

protected Query getNativeQuery1(EntityManager entityManager) {
    return entityManager.createNativeQuery("""
        select p.id, p.title, c.review *
        from post_comment c
        join post p on p.id = c.post_id 
		""")
    .setFirstResult(10)
    .setMaxResults(20)
    .setHint(QueryHints.HINT_FETCH_SIZE, 20);
}

protected Query getNativeQuery2(EntityManager entityManager) {
    return entityManager.createNativeQuery("""
        select c.*, p.*
        from post_comment c
        join post p on p.id = c.post_id
        where p.title like :title
		""")
    .unwrap(NativeQuery.class)
    .addEntity(PostComment.class)
    .addEntity(Post.class);
}

Измерения будут проводиться следующим образом:

protected void compileQueries(
        Function query1,
        Function query2) {

    LOGGER.info("Warming up");

    doInJPA(entityManager -> {
        for (int i = 0; i < 10000; i++) {
            query1.apply(entityManager);
            
            query2.apply(entityManager);
        }
    });

    LOGGER.info(
        "Compile queries for plan cache size {}", 
        planCacheMaxSize
    );

    doInJPA(entityManager -> {
        for (int i = 0; i < 2500; i++) {
            long startNanos = System.nanoTime();
            
            query1.apply(entityManager);
            
            timer.update(
                System.nanoTime() - startNanos, 
                TimeUnit.NANOSECONDS
            );

            startNanos = System.nanoTime();
            
            query2.apply(entityManager);
            
            timer.update(
                System.nanoTime() - startNanos, 
                TimeUnit.NANOSECONDS
            );
        }
    });

    logReporter.report();
}

И методы тестирования JUnit могут просто вызывать компиляционные запросы метод, подобный этому:

@Test
public void testEntityQueries() {
    compileQueries(
        this::getEntityQuery1, 
        this::getEntityQuery2
    );
}

@Test
public void testNativeQueries() {
    compileQueries(
        this::getNativeQuery1, 
        this::getNativeQuery2
    );
}

Размер кэша плана будет изменяться с помощью функции @Параметризованный JUnit:

private final int planCacheMaxSize;

public PlanCacheSizePerformanceTest(
        int planCacheMaxSize) {
    this.planCacheMaxSize = planCacheMaxSize;
}

@Parameterized.Parameters
public static Collection rdbmsDataSourceProvider() {
    List planCacheMaxSizes = new ArrayList<>();
    
    planCacheMaxSizes.add(new Integer[] {1});
    planCacheMaxSizes.add(new Integer[] {100});
    
    return planCacheMaxSizes;
}

@Override
protected void additionalProperties(
        Properties properties) {
    properties.put(
        "hibernate.query.plan_cache_max_size",
        planCacheMaxSize
    );

    properties.put(
        "hibernate.query.plan_parameter_metadata_max_size",
        planCacheMaxSize
    );
}

Таким образом, мы изменим размер кэша QueryPlanCache и ParameterMetadata от 1 до 100. Когда размер кэша плана равен 1, запросы всегда будут компилироваться, в то время как при размере кэша плана 100 планы запросов будут обслуживаться из кэша.

При выполнении вышеупомянутых модульных тестов мы получим следующие результаты.

Производительность кэша плана запросов к сущностям JPQL

Как вы можете ясно видеть, запросы сущностей JPQL могут значительно улучшить кэш плана запросов, и именно поэтому вам следует убедиться, что находится в спящем режиме.query.plan_cache_max_size может вместить подавляющее большинство запросов сущностей, необходимых для запуска вашего приложения.

QueryPlanCache влияет как на запросы JPQL, так и на запросы API критериев, поскольку запросы критериев переводятся в JPQL.

Производительность кэша собственного плана запросов

Хотя это и не так впечатляюще, как для запросов JPQL, кэш плана запросов также может ускорить собственные запросы, поэтому убедитесь, что вы установили режим гибернации.запрос.plan_parameter_metadata_max_size право собственности на конфигурацию.

Очевидного повышения производительности при использовании именованных запросов по сравнению с динамическими нет, поскольку за кулисами именованный запрос может кэшировать только свое определение (например, NamedQueryDefinition ), а фактический кэш плана запроса доступен как для динамических, так и для именованных запросов.

Наиболее важные параметры, которые необходимо принять во внимание, – это те, которые управляют кэшем плана запросов Hibernate.

Для запросов сущностей кэш плана действительно может повлиять на производительность. Для собственных запросов выигрыш менее значителен.

Кэш плана, в котором хранятся как сущностные, так и собственные запросы, важно настроить его размер таким образом, чтобы он мог вместить все выполняемые запросы. В противном случае некоторые запросы сущностей, возможно, придется перекомпилировать, что приведет к увеличению времени ответа на текущую транзакцию.