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

Разбиение запросов на страницы с помощью JPA и гибернации

Узнайте, как использовать разбиение на страницы с помощью JPA и гибернации, чтобы ограничить размер базового набора результатов JDBC для запросов сущностей или прогнозов DTO.

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

Вдохновленный этим ответом на StackOverflow Я недавно дал, я решил, что пришло время написать статью о разбиении запросов на страницы при использовании JPA и Hibernate.

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

Как использовать разбиение запросов на страницы в #Hibernate , чтобы ограничить размер набора результатов JDBC и избежать получения большего количества данных, чем необходимо. @vlad_mihalcea https://t.co/fkd8ne1mYj pic.twitter.com/Ca78OhlIP1

Теперь давайте предположим, что мы определили следующие классы сущностей Post и Post Comment в нашем приложении:

Класс Post является родительской сущностью, в то время как комментарий к сообщению является дочерним, поскольку у него есть @ManyToOne связь с Post сущностью. Обе сущности реализуют Идентифицируемый интерфейс, который предоставляет контракт для доступа к базовому идентификатору сущности.

Далее мы собираемся сохранить следующие Сообщение и Комментарий к сообщению сущности в базе данных:

LocalDateTime timestamp = LocalDateTime.of(
    2018, 10, 9, 12, 0, 0, 0
);

int commentsSize = 5;

LongStream.range(1, 50).forEach(postId -> {
    Post post = new Post();
    post.setId(postId);
    post.setTitle(
        String.format("Post nr. %d", postId)
    );
    post.setCreatedOn(
         Timestamp.valueOf(
            timestamp.plusMinutes(postId)
        )
    );

    LongStream.range(1, commentsSize + 1).forEach(commentOffset -> {
        long commentId = ((postId - 1) * commentsSize) + commentOffset;

        PostComment comment = new PostComment();        
        comment.setId(commentId);
        comment.setReview(
            String.format("Comment nr. %d", comment.getId())
        );
        comment.setCreatedOn(
            Timestamp.valueOf(
                timestamp.plusMinutes(commentId)
            )
        );

        post.addComment(comment);

    });
    
    entityManager.persist(post);
});

Чтобы ограничить размер базового запроса Результирующего набора , интерфейс JPA Запрос предоставляет setMaxResults метод .

Поэтому при выполнении следующего JPQL-запроса:

List posts = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "order by p.createdOn ")
.setMaxResults(10)
.getResultList();

assertEquals(10, posts.size());
assertEquals("Post nr. 1", posts.get(0).getTitle());
assertEquals("Post nr. 10", posts.get(9).getTitle());

Hibernate генерирует следующую инструкцию SQL на PostgreSQL:

SELECT p.id AS id1_0_,
       p.created_on AS created_2_0_,
       p.title AS title3_0_
FROM post p
ORDER BY p.created_on
LIMIT 10

В SQL Server 2012 (или более поздней версии) Hibernate выполнит следующий SQL-запрос:

SELECT p.id AS id1_0_,
       p.created_on AS created_2_0_,
       p.title AS title3_0_
FROM post p
ORDER BY p.created_on
OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY

Таким образом, запрос разбиения на страницы SQL адаптирован к базовым возможностям ядра базы данных.

Использование ORDER BY является обязательным при использовании разбиения запросов на страницы, поскольку SQL не гарантирует какой-либо конкретный порядок, если мы не предоставим его с помощью предложения ORDER BY .

Если предыдущий запрос был типичным для первой страницы данного запроса разбиения на страницы, переход на следующую страницу требует размещения результирующего набора там, где заканчивалась последняя страница. Для этой цели интерфейс JPA Query предоставляет setFirstResult метод .

List posts = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "order by p.createdOn ")
.setFirstResult(10)
.setMaxResults(10)
.getResultList();

assertEquals(10, posts.size());
assertEquals("Post nr. 11", posts.get(0).getTitle());
assertEquals("Post nr. 20", posts.get(9).getTitle());

При выполнении предыдущего запроса JPQL на PostgreSQL Hibernate выполняет следующие инструкции SQL SELECT:

SELECT p.id AS id1_0_,
       p.created_on AS created_2_0_,
       p.title AS title3_0_
FROM post p
ORDER BY p.created_on
LIMIT 10
OFFSET 10

а в SQL Server 2012 (или новее) Hibernate создаст этот SQL-запрос:

SELECT p.id AS id1_0_,
       p.created_on AS created_2_0_,
       p.title AS title3_0_
FROM post p
ORDER BY p.created_on
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY

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

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

public class PostCommentSummary {

    private Number id;
    private String title;
    private String review;

    public PostCommentSummary(
            Number id, 
            String title, 
            String review) {
        this.id = id;
        this.title = title;
        this.review = review;
    }

    public PostCommentSummary() {}

    //Getters omitted for brevity
}

При выполнении следующего запроса проекции DTO:

List summaries = entityManager
.createQuery(
    "select new " +
    "   com.vladmihalcea.book.hpjp.hibernate.fetching.PostCommentSummary( " +
    "       p.id, p.title, c.review " +
    "   ) " +
    "from PostComment c " +
    "join c.post p " +
    "order by c.createdOn")
.setMaxResults(10)
.getResultList();

assertEquals(10, summaries.size());
assertEquals("Post nr. 1", summaries.get(0).getTitle());
assertEquals("Comment nr. 1", summaries.get(0).getReview());

assertEquals("Post nr. 2", summaries.get(9).getTitle());
assertEquals("Comment nr. 10", summaries.get(9).getReview());

Hibernate добавляет предложение разбиения на страницы к базовому SQL-запросу:

SELECT p.id AS col_0_0_,
       p.title AS col_1_0_,
       c.review AS col_2_0_
FROM post_comment c
INNER JOIN post p ON c.post_id=p.id
ORDER BY c.created_on
LIMIT 10

Для получения более подробной информации о проекции DTO с помощью JPA и гибернации ознакомьтесь с этой статьей .

Разбиение запросов JPA на страницы не ограничивается запросами сущностей, такими как JPQL или API критериев. Вы также можете использовать его для собственных SQL-запросов.

List posts = entityManager
.createNativeQuery(
    "select p.id as id, p.title as title " +
    "from post p " +
    "order by p.created_on", Tuple.class)
.setFirstResult(10)
.setMaxResults(10)
.getResultList();

assertEquals(10, posts.size());
assertEquals("Post nr. 11", posts.get(0).get("title"));
assertEquals("Post nr. 20", posts.get(9).get("title"));

При выполнении приведенного выше SQL-запроса Hibernate добавляет предложение разбиения на страницы для конкретной базы данных:

SELECT p.id AS id,
       p.title AS title
FROM post p
ORDER BY p.created_on
LIMIT 10
OFFSET 10

Однако, если мы попытаемся использовать предложение JOIN FETCH в запросе сущности, одновременно используя разбиение на страницы JPA:

List posts = entityManager.createQuery(
    "select p " +
    "from Post p " +
    "left join fetch p.comments " +
    "order by p.createdOn", Post.class)
.setMaxResults(10)
.getResultList();

assertEquals(10, posts.size());

Режим гибернации выдаст следующее предупреждающее сообщение:

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

И в выполняемом SQL-запросе будет отсутствовать предложение разбиения на страницы:

SELECT p.id AS id1_0_0_,
       c.id AS id1_1_1_,
       p.created_on AS created_2_0_0_,
       p.title AS title3_0_0_,
       c.created_on AS created_2_1_1_,
       c.post_id AS post_id4_1_1_,
       c.review AS review3_1_1_,
       c.post_id AS post_id4_1_0__,
       c.id AS id1_1_0__
FROM post p
LEFT OUTER JOIN post_comment c ON p.id=c.post_id
ORDER BY p.created_on

Это связано с тем, что Hibernate хочет полностью извлекать сущности вместе с их коллекциями, как указано в предложении JOIN FETCH , в то время как разбиение на страницы на уровне SQL может привести к усечению набора результатов , возможно, оставив родительскую публикацию сущность с меньшим количеством элементов в комментариях коллекции.

Проблема с предупреждением HHH000104 заключается в том, что Hibernate будет извлекать продукт сущностей Post и PostComment , и из-за размера результирующего набора время ответа на запрос будет значительным.

Чтобы обойти это ограничение, вам необходимо использовать запрос оконной функции:

List posts = entityManager
.createNativeQuery(
    "select * " +
    "from (   " +
    "    select *, dense_rank() OVER (ORDER BY post_id) rank " +
    "    from (   " +
    "        select p.*, pc.* " +
    "        from post p  " +
    "        left join post_comment pc on p.id = pc.post_id  " +
    "        order by p.created_on " +
    "    ) p_pc " +
    ") p_pc_r " +
    "where p_pc_r.rank <= :rank", Post.class)
.setParameter("rank", 10)
.unwrap(NativeQuery.class)
.addEntity("p", Post.class)
.addEntity("pc", PostComment.class)
.setResultTransformer(DistinctPostResultTransformer.INSTANCE)
.getResultList();

Для получения более подробной информации об использовании оконных функций для устранения проблемы HHH000104 , а также кода DistinctPostResultTransformer , ознакомьтесь с этой статьей .

JPA 2.2 добавил метод getResultStream Query , который, как вам может показаться, является допустимой альтернативой разбиению на страницы. Однако результат потока не предоставит планировщику запросов размер результирующего набора, поэтому может быть выбран неоптимальный план выполнения. По этой причине гораздо эффективнее использовать разбиение на страницы, чем потоковую передачу, когда дело доходит до извлечения небольших объемов данных.

Для получения более подробной информации о том, почему разбиение на страницы более эффективно, чем потоковая передача, ознакомьтесь с этой статьей .

Маркус Винанд, написавший книгу “Объяснение производительности SQL”, выступает за Разбиение набора ключей на страницы вместо смещения . Хотя разбиение на страницы со смещением является стандартной функцией SQL, есть две причины, по которым вы предпочли бы разбиение на страницы набора ключей:

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

Даже если Hibernate не поддерживает разбиение набора ключей на страницы, для этой цели можно использовать собственный SQL-запрос. Я освещаю эту тему в своем обучении высокопроизводительному SQL.

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

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