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

Лучший способ использовать сопоставление JPA SqlResultSetMapping

Узнайте, как использовать JPA SqlResultSetMapping, EntityResult, ConstructorResult и ColumnResult для сопоставления сущностей, DTO и значений столбцов с помощью Hibernate.

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

Вступление

В этой статье я собираюсь объяснить, как использовать сопоставление JPA SQLRESULTSET, а также параметры Entity Result, ConstructorResult и ColumnResult.

Модель предметной области

Давайте рассмотрим, что у нас есть следующие таблицы post и post_comment в нашей базе данных:

Мы собираемся создать 50 записей строк, каждая запись с 5 дочерними записями//post_comment//.

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

LongStream.rangeClosed(1, POST_COUNT)
.forEach(postId -> {
    Post post = new Post()
    .setId(postId)
    .setTitle(
        String.format(
            "High-Performance Java Persistence - Chapter %d",
            postId
        )
    )
    .setCreatedOn(
        Timestamp.valueOf(timestamp.plusDays(postId))
    );

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

        post.addComment(
            new PostComment()
            .setId(commentId)
            .setReview(
                String.format("Comment nr. %d - A must read!", commentId)
            )
            .setCreatedOn(
                Timestamp.valueOf(
                    timestamp
                    .plusDays(postId)
                    .plusMinutes(commentId)
                )
            )
        );

    });

    entityManager.persist(post);
});

Затем мы выполним несколько собственных SQL-запросов и посмотрим, как мы можем извлекать DTO, сущности или смешивать сущности со скалярными значениями.

Сопоставление JPA SqlResultSetMapping

Аннотация SqlResultSetMapping JPA выглядит следующим образом:

@Repeatable(SqlResultSetMappings.class)
@Target({TYPE}) 
@Retention(RUNTIME)
public @interface SqlResultSetMapping { 

    String name(); 

    EntityResult[] entities() default {};

    ConstructorResult[] classes() default {};

    ColumnResult[] columns() default {};
}

Аннотация SqlResultSetMapping повторяется и применяется на уровне класса сущностей. Помимо присвоения уникального имени, которое используется Hibernate для регистрации сопоставления, существует три варианта сопоставления:

  • Результат сущности
  • Результат конструктора
  • Результат столбца

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

JPA SqlResultSetMapping – EntityResult

Параметр Результат сущности позволяет сопоставить столбцы JDBC Набор результатов с одной или несколькими сущностями JPA.

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

Как я объяснил в этой статье , мы можем использовать DENSE_RANK Функцию окна SQL , чтобы знать, как фильтровать post и post_comment присоединенные записи, как показано в следующем SQL-запросе:

SELECT *
FROM (
  SELECT
    *,
    DENSE_RANK() OVER (
    ORDER BY
      "p.created_on",
      "p.id"
    ) rank
  FROM (
    SELECT
      p.id AS "p.id", p.created_on AS "p.created_on",
      p.title AS "p.title", pc.post_id AS "pc.post_id",
      pc.id as "pc.id", pc.created_on AS "pc.created_on",
      pc.review AS "pc.review"
    FROM post p
    LEFT JOIN post_comment pc ON p.id = pc.post_id
    WHERE p.title LIKE :titlePattern
    ORDER BY p.created_on
  ) p_pc
) p_pc_r
WHERE p_pc_r.rank <= :rank

Однако мы не хотим возвращать список значений скалярных столбцов. Мы хотим вернуть сущности JPA из этого запроса, поэтому нам нужно настроить атрибут entities аннотации @SqlResultSetMapping , как это:

@NamedNativeQuery(
    name = "PostWithCommentByRank",
    query = """
        SELECT *
        FROM (
          SELECT
            *,
            DENSE_RANK() OVER (
            ORDER BY
              "p.created_on",
              "p.id"
            ) rank
          FROM (
            SELECT
              p.id AS "p.id", p.created_on AS "p.created_on",
              p.title AS "p.title", pc.post_id AS "pc.post_id",
              pc.id as "pc.id", pc.created_on AS "pc.created_on",
              pc.review AS "pc.review"
            FROM post p
            LEFT JOIN post_comment pc ON p.id = pc.post_id
            WHERE p.title LIKE :titlePattern
            ORDER BY p.created_on
          ) p_pc
        ) p_pc_r
        WHERE p_pc_r.rank <= :rank
        """,
    resultSetMapping = "PostWithCommentByRankMapping"
)
@SqlResultSetMapping(
    name = "PostWithCommentByRankMapping",
    entities = {
        @EntityResult(
            entityClass = Post.class,
            fields = {
                @FieldResult(name = "id", column = "p.id"),
                @FieldResult(name = "createdOn", column = "p.created_on"),
                @FieldResult(name = "title", column = "p.title"),
            }
        ),
        @EntityResult(
            entityClass = PostComment.class,
            fields = {
                @FieldResult(name = "id", column = "pc.id"),
                @FieldResult(name = "createdOn", column = "pc.created_on"),
                @FieldResult(name = "review", column = "pc.review"),
                @FieldResult(name = "post", column = "pc.post_id"),
            }
        )
    }
)

С помощью SqlResultSetMapping на месте мы можем получить Сообщение и Комментарий к сообщению сущности, подобные этой:

List postAndCommentList = entityManager
    .createNamedQuery("PostWithCommentByRank")
    .setParameter("titlePattern", "High-Performance Java Persistence %")
    .setParameter("rank", POST_RESULT_COUNT)
    .getResultList();

И мы можем проверить, правильно ли выбраны объекты:

assertEquals(
    POST_RESULT_COUNT * COMMENT_COUNT, 
    postAndCommentList.size()
);

for (int i = 0; i < COMMENT_COUNT; i++) {
    Post post = (Post) postAndCommentList.get(i)[0];
    PostComment comment = (PostComment) postAndCommentList.get(i)[1];

    assertTrue(entityManager.contains(post));
    assertTrue(entityManager.contains(comment));

    assertEquals(
        "High-Performance Java Persistence - Chapter 1",
        post.getTitle()
    );

    assertEquals(
        String.format(
            "Comment nr. %d - A must read!",
            i + 1
        ),
        comment.getReview()
    );
}

@EntityResult также полезен при извлечении сущностей JPA с помощью хранимых процедур SQL. Ознакомьтесь с этой статьей для получения более подробной информации.

JPA SqlResultSetMapping – конструкторрезультат

Предположим, что мы хотим выполнить запрос агрегирования, который подсчитывает количество записей post_comment для каждой записи и возвращает запись заголовок для целей отчетности. Для достижения этой цели мы можем использовать следующий SQL-запрос:

SELECT
  p.id AS "p.id",
  p.title AS "p.title",
  COUNT(pc.*) AS "comment_count"
FROM post_comment pc
LEFT JOIN post p ON p.id = pc.post_id
GROUP BY p.id, p.title
ORDER BY p.id

Мы также хотим инкапсулировать заголовок поста и количество комментариев в следующем:

public class PostTitleWithCommentCount {

    private final String postTitle;
    
    private final int commentCount;

    public PostTitleWithCommentCount(
            String postTitle,
            int commentCount) {
        this.postTitle = postTitle;
        this.commentCount = commentCount;
    }

    public String getPostTitle() {
        return postTitle;
    }

    public int getCommentCount() {
        return commentCount;
    }
}

Чтобы сопоставить результирующий набор приведенного выше SQL-запроса с Заголовком публикации с количеством комментариев Кроме того, мы можем использовать атрибут classes аннотации @SqlResultSetMapping , например:

@NamedNativeQuery(
    name = "PostTitleWithCommentCount",
    query = """
        SELECT
          p.id AS "p.id",
          p.title AS "p.title",
          COUNT(pc.*) AS "comment_count"
        FROM post_comment pc
        LEFT JOIN post p ON p.id = pc.post_id
        GROUP BY p.id, p.title
        ORDER BY p.id
        """,
    resultSetMapping = "PostTitleWithCommentCountMapping"
)
@SqlResultSetMapping(
    name = "PostTitleWithCommentCountMapping",
    classes = {
        @ConstructorResult(
            columns = {
                @ColumnResult(name = "p.title"),
                @ColumnResult(name = "comment_count", type = int.class)
            },
            targetClass = PostTitleWithCommentCount.class
        )
    }
)

Аннотация Результат конструктора позволяет нам указать Hibernate, какой класс DTO использовать, а также какой конструктор вызывать при создании экземпляров объектов DTO.

Обратите внимание, что мы использовали атрибут type аннотации @ColumnResult , чтобы указать, что comment_count должен быть приведен к Java int . Это необходимо, так как некоторые драйверы JDBC используют либо Long , либо BigInteger для результатов функции агрегирования SQL.

Вот как вы можете вызвать Заголовок сообщения С количеством комментариев именованный собственный запрос с помощью JPA:

List postTitleAndCommentCountList = entityManager
    .createNamedQuery("PostTitleWithCommentCount")
    .setMaxResults(POST_RESULT_COUNT)
    .getResultList();

И мы видим, что возвращенный Заголовок сообщения С количеством комментариев DTO был выбран правильно:

assertEquals(POST_RESULT_COUNT, postTitleAndCommentCountList.size());

for (int i = 0; i < POST_RESULT_COUNT; i++) {
    PostTitleWithCommentCount postTitleWithCommentCount = 
        postTitleAndCommentCountList.get(i);

    assertEquals(
        String.format(
            "High-Performance Java Persistence - Chapter %d",
            i + 1
        ),
        postTitleWithCommentCount.getPostTitle()
    );

    assertEquals(COMMENT_COUNT, postTitleWithCommentCount.getCommentCount());
}

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

JPA SqlResultSetMapping – ColumnResult

Предыдущий пример показал, как мы могли бы сопоставить результирующий набор агрегации SQL с DTO. Но что, если мы хотим вернуть объект JPA, для которого мы подсчитываем комментарии?

Для достижения этой цели мы можем использовать атрибут entities для определения Post сущности, которую мы извлекаем, и атрибут classes аннотации @SqlResultSetMapping для отображения скалярного значения, которое в нашем случае является числом связанных post_comment записей:

@NamedNativeQuery(
    name = "PostWithCommentCount",
    query = """
        SELECT
          p.id AS "p.id",
          p.title AS "p.title",
          p.created_on AS "p.created_on",
          COUNT(pc.*) AS "comment_count"
        FROM post_comment pc
        LEFT JOIN post p ON p.id = pc.post_id
        GROUP BY p.id, p.title
        ORDER BY p.id
        """,
    resultSetMapping = "PostWithCommentCountMapping"
)
@SqlResultSetMapping(
    name = "PostWithCommentCountMapping",
    entities = @EntityResult(
        entityClass = Post.class,
        fields = {
            @FieldResult(name = "id", column = "p.id"),
            @FieldResult(name = "createdOn", column = "p.created_on"),
            @FieldResult(name = "title", column = "p.title"),
        }
    ),
    columns = @ColumnResult(
        name = "comment_count",
        type = int.class
    )
)

При выполнении Сообщения С количеством комментариев именованный собственный запрос:

List postWithCommentCountList = entityManager
    .createNamedQuery("PostWithCommentCount")
    .setMaxResults(POST_RESULT_COUNT)
    .getResultList();

мы получим как Запись сущность, так и количество комментариев значение скалярного столбца:

assertEquals(POST_RESULT_COUNT, postWithCommentCountList.size());

for (int i = 0; i < POST_RESULT_COUNT; i++) {
    Post post = (Post) postWithCommentCountList.get(i)[0];
    int commentCount = (int) postWithCommentCountList.get(i)[1];

    assertTrue(entityManager.contains(post));

    assertEquals(i + 1, post.getId().intValue());
    assertEquals(
        String.format(
            "High-Performance Java Persistence - Chapter %d",
            i + 1
        ),
        post.getTitle()
    );

    assertEquals(COMMENT_COUNT, commentCount);
}

Вывод

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

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

Хотя использование этого в качестве API, аналогичного Hibernate ResultTransformer , было бы гораздо лучшим способом построения сопоставления набора результатов SQL по сравнению с декларативным подходом, заданным аннотацией @SqlResultSetMapping , пока спецификация JPA не предоставит программный подход, вы можете использовать аннотацию @SqlResultSetMapping для этой задачи.