Введение

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

Советы в этой статье основаны на проделанной мной работе по добавлению поддержки Spring 6 в Hypersistence Optimizer и проекте High-Performance Java Persistence.

Java 17

Прежде всего, Spring 6 увеличил минимальную версию Java до 17, и это замечательно, потому что теперь вы можете использовать текстовые блоки и записи.

Текстовые блоки

Благодаря текстовым блокам ваши аннотации @Query станут более читабельными:

@Query("""
    select p
    from Post p
    left join fetch p.comments
    where p.id between :minId and :maxId
    """)
List<Post> findAllWithComments(
    @Param("minId") long minId,
    @Param("maxId") long maxId
);

Дополнительные сведения о текстовых блоках Java также см. в этой статье.

Records

Java Records отлично подходят для DTO-проекций. Например, вы можете определить класс PostRecord следующим образом:

public record PostCommentRecord(
    Long id,
    String title,
    String review
) {}

Затем вы можете получить объекты PostCommentRecord с помощью метода запроса Spring Data JPA:

@Query("""
    select new PostCommentRecord(
        p.id as id,
        p.title as title,
        c.review as review
    )
    from PostComment c
    join c.post p
    where p.title like :postTitle
    order by c.id
    """)
List<PostCommentRecord> findCommentRecordByTitle(
    @Param("postTitle") String postTitle
);

Причина, по которой мы могли использовать простое имя Java-класса PostCommentRecord в выражении конструктора JPQL, заключается в том, что я зарегистрировал следующий ClassImportIntegrator из проекта Hibernate Types:

properties.put(
    "hibernate.integrator_provider",
    (IntegratorProvider) () -> Collections.singletonList(
        new ClassImportIntegrator(
            List.of(
                PostCommentRecord.class
            )
        )
    )
);

Дополнительные сведения о Java Records см. в этой статье.

И это еще не все! В Java 17 улучшено сообщение об ошибке NullPointerException и добавлено сопоставление с шаблоном для switch и instanceOf.

JPA 3.1

По умолчанию Spring 6 использует Hibernate 6.1, который, в свою очередь, использует Jakarta Persistence 3.1.

Версия 3.0 знаменует собой миграцию с Java Persistence на Jakarta Persistence, поэтому по этой причине вам придется заменить импорты пакета javax.persistence на пространство имен jakarta.persistence.

Это самое значительное изменение, которое вам придется сделать, чтобы перейти на JPA 3. Тем временем была выпущена версия 3.1, но она включает лишь некоторые незначительные улучшения, которые уже поддерживались Hibernate.

Атрибуты объекта UUID

Например, JPA 3 теперь поддерживает базовые типы UUID:

@Column(
    name = "external_id",
    columnDefinition = "UUID NOT NULL"
)
private UUID externalId;

И вы даже можете использовать их в качестве идентификаторов сущностей:

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Но это просто ужасная идея, потому что использование UUID для первичного ключа вызовет много проблем:

  • Страницы индекса будут редко заполненными, потому что каждый новый UUID будет добавляться случайным образом в кластеризованный индекс B+Tree.

  • Будет больше разбиений страниц из-за случайности значений первичного ключа

  • UUID огромен, ему требуется в два раза больше байтов, чем столбцу bigint. Это влияет не только на первичный ключ, но и на все связанные с ним внешние ключи.

Более того, если вы используете SQL Server, MySQL или MariaDB, таблица по умолчанию будет организована как Clustered Index, что еще больше усугубит все эти проблемы.

Поэтому лучше избегать использования UUID для идентификаторов сущностей. Если вам действительно нужно генерировать уникальные идентификаторы из приложения, вам лучше вместо этого использовать 64-битный случайный TSID с сортировкой по времени.

Более подробно о том, почему стандартные UUID не подходят для первичных ключей и почему вместо них следует использовать TSID с временной сортировкой, читайте в этой статье.

Новые функции JPQL

В JPQL было дополнено много новых функций, например, числовые функции CEILING, FLOOR, EXP, LN, POWER, ROUND, SIGN.

Однако наиболее полезной из найденных мной является Date/Time функция EXTRACT:

List<Post> posts = entityManager.createQuery("""
    select p
    from Post p
    where EXTRACT(YEAR FROM createdOn) = :year
    """, Post.class)
.setParameter("year", Year.now().getValue())
.getResultList();

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

Автоматически закрывающиеся EntityManager и EntityManagerFactory

В то время как Hibernate Sessionи SessionFactory уже расширяли интерфейс AutoClosable, теперь JPA EntityManager и EntityManagerFactoryя также последовали этой практике:

Хотя вам редко придется полагаться на это, поскольку Spring позаботится об EntityManager от вашего имени, это очень удобно, когда вам нужно программно обрабатывать EntityManager.

Hibernate 6

В то время как Java 17 и JPA 3.1 предоставляют вам несколько функций, Hibernate 6 обеспечивает массу улучшений.

Оптимизация JDBC

Раньше Hibernate считывал значения столбца JDBC ResultSet, используя псевдоним соответствующего столбца, и это оказалось медленным. По этой причине Hibernate 6 перешел на чтение значений основных столбцов по их положению в базовой проекции SQL.

Помимо ускорения, есть очень приятный побочный эффект от внесения этого изменения. Базовые SQL-запросы теперь более читабельны.

Например, если вы запустите этот запрос JPQL в Hibernate 5:

Post post = entityManager.createQuery("""
    select p
    from Post p
    join fetch p.comments
    where p.id = :id
    """, Post.class)
.setParameter("id", 1L)
.getSingleResult();

Будет выполнен следующий SQL-запрос:

SELECT
    bidirectio0_.id AS id1_0_0_,
    comments1_.id AS id1_1_1_,
    bidirectio0_.title AS title2_0_0_,
    comments1_.post_id AS post_id3_1_1_,
    comments1_.review AS review2_1_1_,
    comments1_.post_id AS post_id3_1_0__,
    comments1_.id AS id1_1_0__
FROM post
    bidirectio0_
INNER JOIN
    post_comment comments1_ ON bidirectio0_.id=comments1_.post_id
WHERE
    bidirectio0_.id=1

Уродливо!

Как, если вы запустите тот же JPQL в Hibernate 6, вместо него будет выполнен следующий SQL-запрос:

SELECT
    p1_0.id,
    c1_0.post_id,
    c1_0.id,
    c1_0.review,
    p1_0.title
FROM
    post p1_0
JOIN
    post_comment c1_0 ON p1_0.id=c1_0.post_id
WHERE
    p1_0.id = 1

Гораздо лучше, верно?

Семантическая модель запроса и запросы Criteria API

Hibernate 6 предоставляет совершенно новый синтаксический анализатор запросов к сущностям, который способен создавать каноническую модель, модель семантического запроса, как из JPQL, так и из Criteria API.

Благодаря унификации модели запросов к сущностям запросы Criteria API теперь могут быть расширены возможностями, которые не поддерживаются Jakarta Persistence, например производными таблицами или общими табличными выражениями.

Более подробную информацию о семантической модели запросов Hibernate (Semantic Query Model) можно найти в этой статье.

Устаревший Hibernate Criteria был удален, но API Criteria был дополнен множеством новых функций, которые доступны через HibernateCriteriaBuilder.

Например, вы можете использовать функцию ilike для сопоставления LIKE без учета регистра:

HibernateCriteriaBuilder builder = entityManager
    .unwrap(Session.class)
    .getCriteriaBuilder();
 
CriteriaQuery<Post> criteria = builder.createQuery(Post.class);
Root<Post> post = criteria.from(Post.class);
 
ParameterExpression<String> parameterExpression = builder
    .parameter(String.class);
     
List<Post> posts = entityManager.createQuery(
    criteria
        .where(
            builder.ilike(
                post.get(Post_.TITLE),
                parameterExpression)
            )
        .orderBy(
            builder.asc(
                post.get(Post_.ID)
            )
        )
)
.setParameter(parameterExpression, titlePattern)
.setMaxResults(maxCount)
.getResultList();

Однако это лишь базовый пример. С новым HibernateCriteriaBuilder теперь вы можете представлять:

Усовершенствования Dialect

В то время как в Hibernate 5 вам нужно было выбирать из множества версий Dialect в зависимости от базовой версии базы данных, в Hibernate 6 это было значительно упрощено:

Более того, вам даже не нужно указывать Dialect в конфигурации Spring, так как он может быть определен из JDBC DatabaseMetaData.

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

Автоматическая дедупликация

Вы помните, как раздражало использование ключевого слова DISTINCT для дедупликации сущностей всякий раз, когда вы использовали JOIN FETCH?

List<Post> posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.comments
    where p.title = :title
    """, Post.class)
.setParameter("title", "High-Performance Java Persistence")
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getResultList();

Если вы забыли отправить подсказку PASS_DISTINCT_THROUGH, то Hibernate 5 передаст ключевое слово DISTINCT в SQL-запрос и заставит план выполнения выполнить несколько дополнительных шагов, которые только замедлят выполнение вашего запроса:

Unique 
  (cost=23.71..23.72 rows=1 width=1068)
  (actual time=0.131..0.132 rows=2 loops=1)
  ->  Sort 
        (cost=23.71..23.71 rows=1 width=1068)
        (actual time=0.131..0.131 rows=2 loops=1)
        Sort Key: p.id, pc.id, p.created_on, pc.post_id, pc.review
        Sort Method: quicksort  Memory: 25kB
        ->  Hash Right Join 
            (cost=11.76..23.70 rows=1 width=1068)
            (actual time=0.054..0.058 rows=2 loops=1)
              Hash Cond: (pc.post_id = p.id)
              ->  Seq Scan on post_comment pc 
                  (cost=0.00..11.40 rows=140 width=532)
                  (actual time=0.010..0.010 rows=2 loops=1)
              ->  Hash 
                   (cost=11.75..11.75 rows=1 width=528)
                   (actual time=0.027..0.027 rows=1 loops=1)
                    Buckets: 1024  Batches: 1  Memory Usage: 9kB
                    ->  Seq Scan on post p 
                        (cost=0.00..11.75 rows=1 width=528)
                        (actual time=0.017..0.018 rows=1 loops=1)
                          Filter: (
                            (title)::text =
                            'High-Performance Java Persistence eBook has been released!'::text
                          )
                          Rows Removed by Filter: 3

Более подробно о том, как работает DISTINCT в JPA читайте в этой статье.

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

List<Post> posts = entityManager.createQuery("""
    select p
    from Post p
    left join fetch p.comments
    where p.title = :title
    """, Post.class)
.setParameter("title", "High-Performance Java Persistence")
.getResultList();

Потрясающе, правда?

Вывод

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

Например, Hibernate 6 предоставляет множество оптимизаций и новых возможностей, которые позволят решить многие из ваших ежедневных требований к доступу к данным.

Комментарии (2)


  1. novoselov
    22.12.2022 21:02
    +10

    В статье про Spring 6 описание нововведений в Java 17, JPA 3.1, Hibernate 6 и ни слова про сам Spring 6?


    1. aleksandy
      23.12.2022 11:48

      Ну, как же ни слова? Целых два

      Spring 6 увеличил минимальную версию Java до 17

      и

      вам даже не нужно указывать Dialect в конфигурации Spring

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

      А вообще ничего удивительного нет, какой ещё обзор мог написать один ведущих из разработчиков Hibernate-а.