Skip to content

Latest commit

 

History

History

Spring_part_10

Spring Boot lessons part 10 - Data JPA Repositories

В папке DOC sql-скрипты и др. полезные файлы.

Док. (ссылки) для изучения:



Для начала проведем предварительную подготовку (первые 3-и шага из предыдущих частей):

Шаг 1. - в файле build.gradle добавим необходимые plugin-ы:

/* 
   Плагин Spring Boot добавляет необходимые задачи в Gradle 
   и имеет обширную взаимосвязь с другими plugin-ами.
*/
id 'org.springframework.boot' version '3.1.3'
/* 
   Менеджер зависимостей позволяет решать проблемы несовместимости 
   различных версий и модулей Spring-а
*/
id "io.spring.dependency-management" version '1.0.11.RELEASE'
/* Подключим Lombok */
id "io.freefair.lombok" version "8.3"

Шаг 2. - подключаем Spring Boot starter:

/* 
   Подключим Spring Boot Starter он включает поддержку 
   авто-конфигурации, логирование и YAML
*/
implementation 'org.springframework.boot:spring-boot-starter'

Шаг 3. - подключаем блок тестирования (Spring Boot Starter Test) (он будет активен на этапе тестирования):

testImplementation 'org.springframework.boot:spring-boot-starter-test'

Шаг 4. - автоматически Gradle создал тестовую зависимость на Junit5 (мы можем использовать как Junit4, так и TestNG):

test {
    useJUnitPlatform()
}

Шаг 5. - подключим блок для работы с БД (Spring Boot Starter Data Jpa):

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

!!! НЕ ЗАБЫВАЕМ !!! У нас есть классы (см. ConnectionPool.java и комментарии), где мы пытаемся внедрить параметры в 
поля через аннотации, с использованием аннатационного же конструктора @RequiredArgsConstructor. Фокус не пройдет без 
создания и настройки файла конфигурации: lombok.config - 'контекст' просто завалится. 

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

Шаг 6. - Для использования средств подобных Hibernate ENVERS подключим такую же поддержку от Spring (начиная с Lesson_59):

implementation 'org.springframework.data:spring-data-envers'

Еще раз повторим что такое Spring Data - DOC/SpringData.txt


Lesson 47 - Repository.

Spring Data JPA предоставляет кроме авто-конфигурации и управления транзакциями еще и автоматическое предоставление DAO-слоя через интерфейс Repository.

Repository - это интерфейс-маркер центрального репозитория. Он не имеет методов и говорит нам о том, что некие классы, в нашем случае это AutoConfiguration при встрече с Repository Interface будет обрабатывать его особым образом. В данном случае это создание слоя Repository.

И так, Repository параметризирован (захватывает) тип области (сущности), которой нужно управлять, а также тип идентификатора той самой области (сущности, т.е. в нашем случае см. CompanyRepository.java - у нас есть сущность Company и у нее есть ее - ID). Общая цель - хранить информацию о типе, а также иметь возможность обнаруживать интерфейсы, расширяющие этот интерфейс, во время сканирования пути к классам для упрощения создания bean-компонентов Spring.

Репозитории области, расширяющие этот интерфейс, могут выборочно предоставлять методы CRUD, просто объявляя методы с той же сигнатурой, что и те, которые объявлены в CrudRepository.

    Пакет: org.springframework.data.repository
    
    Interface Repository<T,ID>
    
    Типы параметров:
    T - тип области управляемой репозиторием (у нас, например: Company, User и т.д.);
    ID - тип ID управляемой репозиторием области (у нас, например, ID Company - Integer);

Теперь перепишем наш CompanyRepository.java, который стал интерфейсом.

Фактически реализацию всех его методов берет на себя Spring. И тогда нам уже не нужен наш собственный CrudRepository.java - его мы удаляем и все упоминания (зависимости от) о нем в других классах (тестах).

Чтобы проверить наш новый CompanyRepository и как он работает, создадим тестовый метод deleteCompanyTest() в CompanyRepositoryTest.java (см. комментарии).

Всей этой магией занимается JpaRepositoriesAutoConfiguration, который создан, для того чтобы искать все интерфейсы (классы) Repository и на их основании создавать конкретные прокси реализации.

    /* Автоматическая настройка репозиториев JPA Spring Data.

    Активируется, когда в контексте настроен bean-компонент типа DataSource, 
    тип Spring Data JPA JpaRepository находится в пути к классам и нет другого 
    существующего настроенного JpaRepository.

    После вступления в силу, автоматическая настройка эквивалентна включению 
    репозиториев JPA с использованием аннотации - @EnableJpaRepositories.
    */

    @AutoConfiguration(after = { HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
    @ConditionalOnBean(DataSource.class)
    @ConditionalOnClass(JpaRepository.class)
    @ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })
    /* Настройки который мы можем менять */
    @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", 
                           name = "enabled", 
                           havingValue = "true",
                           matchIfMissing = true) 
    @Import(JpaRepositoriesImportSelector.class)
    public class JpaRepositoriesAutoConfiguration {
        /* code */
    }

Поскольку мы используем Spring Boot, то и не используем явно аннотацию @EnableJpaRepositories.

На основании авто-конфигурации создание Bean зависимостей происходит в AbstractRepositoryConfigurationSourceSupport в котором есть нужные методы registerBeanDefinitions(). И как уже ранее описывалось все bean-ы в цикле проходят процесс создания и внедрения всех нужных зависимостей.

Несложно догадаться, что работающие 'пустые' методы работают не просто так, весь код реализуется Spring-ом через прокси объекты с использованием элементов АОП - аспектно ориентированного программирования.

Док. для изучения:


Lesson 48 - Создание запросов к базе данных исходя из имен методов.

Принципы работы при создании имен методов эквивалентных генерируемым запросам к БД в наших репозиториях описан в DOC/SpringDataJPATutorial/6_QueriesFromMethodNames.txt, и естественно в официальной документации к Spring-у: JPA Repositories Query methods.

  • MyCompanyRepository.java - пример репозитория использующего имена методов для формирования запросов к БД.
  • MyCompanyRepositoryTest.java - интеграционный тест для проверки работы запросов нашего репозитория.

Результат теста, а точнее то, что мы увидим в консоли зависит от настроек запроса к БД. Если наша сущность Company получит именованный запрос с именем один в один, как в методе репозитория см. ниже, то вид самих запросов сгенерированных Hibernate изменятся - можно на время закомментировать аннотацию @NamedQuery в сущности Company.

См. док.:


Lesson 49 - NamedQuery (теория).

Стратегия генерации запросов описанная выше, проста, изящна, но имеет следующие недостатки:

  • Особенности парсера имен методов определяют, какие запросы мы можем создавать. Если анализатор имени метода не поддерживает необходимое ключевое слово, мы не сможем использовать эту стратегию.
  • Имена методов сложных запросов длинные и некрасивые.
  • Нет поддержки динамических запросов.

См. DOC/SpringDataJPATutorial/6_QueriesFromMethodNames.txt

Применение именованных запросов тоже не лишено недостатков, но это еще один способ создания запросов к БД. См. DOC/SpringDataJPATutorial/8_QueriesWithNamedQueries.txt

При этом мы помним, что Spring JPA используем весь функционал Hibernate, а значит мы можем им воспользоваться.

Особенность именованных запросов в том что они имеют преимущество перед PartTreeJpaQuery, рассмотренные выше, т.е. если у двух запросов будет одинаковое название, то первым в работу пойдет именованный запрос (NamedQuery).

Внесем изменения в нашу сущность Company см. Company.java - добавим аннотацию @NamedQuery и настроим ее.

Поскольку мы внесли изменения в сущность Company, то при запуске тестов, а они точно такие же что и в предыдущем задании отработают, но вид запросов Hibernate будет другим в части запроса 'findCompanyByName':

    Hibernate:
      select
        c1_0.id,
        c1_0.name
      from
        company c1_0
      where
        lower(c1_0.name)=lower(?)

Еще пример именованных запросов: Hibernate_part_4/src/main/java/oldboy/lesson_19/entity_19/Employee.java

В сущности Company в запросе 'findCompanyByNameWithParam' и соответственно у одноименного запроса в MyCompanyRepository.java применяется аннотация @Param.


Lesson 50 - Аннотация @Query.

Создадим копию нашего CompanyRepository и назовем его SecondCompanyRepository.java в нем мы применим аннотацию с прописанным в ней HQL запросом:

    @Query("select c from Company c " +
           "join fetch c.locales cl " +
           "where c.name = :name")

Данная аннотация над методом *.findCompanyByName(String name), который может работать на основе генерации запроса по его имени или за счет именованного запроса, сразу захватывает приоритет и запрос внутри нее выполняется безоговорочно, даже если имя запроса в репозитории совпадает с именем именованного запроса в классе, в нашем случае Company.

  • SecondCompanyRepositoryTest.java - тестовый класс для проверки методов репозитория SecondCompanyRepository.java.

  • UserRepository.java - переписали наш класс в интерфейс и расширили JpaRepository, именно в нем мы еще раз применим аннотацию @Query и распишем HQL и нативные SQL запросы:

      @Query(value = "SELECT u.* FROM users u WHERE u.username = :username",
      nativeQuery = true)
    

Еще больше примеров и разъяснений cм. DOC/SpringDataJPATutorial/7_QueriesWithAnnotation.txt


Lesson 51 - Модификация данных через запросы и использование аннотации @Modifying.

И так мы внесли изменения в UserRepository.java, как и в примерах выше сделали из него интерфейс и расширили JpaRepository<User, Long>, но если в первом случае мы в качестве сущности управления использовали Company - JpaRepository<Company, Integer> и ID - Integer, в случае с User это ID - Long, см. DOC/RepositoryInterfaceAndClass/JpaRepository.txt

  • UserRepository.java - интерфейс в котором мы опробуем функционал модификации данных в БД на примере метода и аннотаций:

      @Modifying
      @Query("update User u " +
             "set u.role = :role " +
             "where u.id in (:ids)")
      int updateRole(Role role, Long... ids);
    

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

  • SecondUserRepository.java - интерфейс в котором аннотация @Modifying метода *.updateRole(Role role, Long... ids) приобретает два параметра см. DOC/ModifyingAnnotationInterface.txt, что позволяет нам в некоторой степени управлять состоянием персистивного контекста и избежать большинства исключений:

      @Modifying(clearAutomatically = true,
                 flushAutomatically = true)
      @Query("update User u " +
             "set u.role = :role " +
             "where u.id in (:ids)")
      int updateRole(Role role, Long... ids);
    

Тестовые классы с пояснениями в комментариях:

См. док.:


Lesson 52 - Специальные параметры в запросах, интерфейс Pageable и класс Sort.

При создании запросов способом парсинга названия метода самого запроса, для опоры при составлении имени метода, приходится использовать две важных таблицы, из документации по Spring Data JPA - Reference Documentation:

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

  • Например, мы хотим найти первый элемент из нашей выборки (в зависимости от БД мы можем использовать ключевые слова First или Top), а также хотим отсортировать нашу выборку (OrderBy) по убыванию (Desc), и тогда, название запроса будет выглядеть как-то так - findTopByOrderByIdDesc, см. SecondUserRepository.java и тестовый метод UserRepositoryThirdTest.java

  • Например, мы хотим получить уже не один результат запроса, а коллекцию из нескольких значений на наш запрос, допустим найти первые три User сущности, у которых день рождения был ранее некой даты - findTop3ByBirthDateBefore, далее мы снова хотим отсортировать набор данных по-убыванию - OrderByBirthDateDesc. И мы получаем очень длинное имя *.findTop3ByBirthDateBeforeOrderByBirthDateDesc(LocalDate birthDate), которое, в принципе, легко читается и примерно понятно, что должен делать данный метод, но хочется сделать его название короче. В параметре метода мы передаем ограничивающую запрос дату. И в консоли мы видим:

      Hibernate:
      select
        u1_0.id,
        u1_0.birth_date,
        u1_0.company_id,
        u1_0.firstname,
        u1_0.lastname,
        u1_0.role,
        u1_0.username
      from
        users u1_0
      where
        u1_0.birth_date<? /* ? - переданный параметр даты */
      order by
        u1_0.birth_date desc fetch first ? rows only
    
  • Естественно при таких запросах нам бы хотелось придать некую динамику нашему запросу, чтобы каждый раз не писать новый метод под новый запрос. Например, в нашем предыдущем запросе мы могли бы передать в метод не один, а два параметра, одним из которых будет параметр класса Sort (или см. DOC/SliceSortAndPageUsing/SortClass.txt). Это параметр будет определять, каким образом мы будем обрабатывать результат нашего запроса. Перепишем наш предыдущий метод и добавим ключевые параметры: List findTop2ByBirthDateBefore(LocalDate birthDate, Sort sort). В тестовом методе *.checkDynamicSortTest() мы используем два варианта создания объектов данного класса, напрямую:

      Sort sortById = Sort.by("id").and(Sort.by("firstname").and(Sort.by("lastname")));
    

А так же с использованием вложенного класса:

    Sort.TypedSort<User> sortBy = Sort.sort(User.class);
    Sort sort = sortBy.by(User::getFirstname).and(sortBy.by(User::getLastname));

См. док.:


Lesson 53 - Интерфейс Pageable и возвращаемые значения Page и Slice.

  • FourthUserRepository.java - интерфейс репозитория демонстрирующий работу Slice и Pageable. Возьмем одноименный метод из прошлого урока *.findAllUserBy(Pageable pageable) и, вместо List, вернем в нем Slice. Проверим результат в тестовом методе класса ForthUserRepositoryTest.java.

      /* 
      Нужно помнить, что, хотя, у нас в БД 5-ть записей, но запрос PageRequest 
      делается с 1-ой стр. (не с 0 - ой), поэтому результаты Slice на экран идут 
      начиная с 3-го ID. Сортировка прямая, т.е. по возрастанию ID. 
      */
      @Test
      void checkSliceTest() {
        PageRequest myPageable = PageRequest.of(1, 2, Sort.by("id"));
        Slice<User> myFirstSlice = fourthUserRepository.findAllUserBy(myPageable);
    
        while (myFirstSlice.hasNext()) {
          myFirstSlice.forEach(user -> System.out.println(user.getId()));
          myFirstSlice = fourthUserRepository.findAllUserBy(myFirstSlice.nextPageable());
          myFirstSlice.forEach(user -> System.out.println(user.getId()));
        }
      }
    
      /* На экране результат работы демо-теста, сначала 1-ый Slice, затем 2-ой */
      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        order by
          u1_0.id offset ? rows fetch first ? rows only
      3
      4
      
      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        order by
          u1_0.id offset ? rows fetch first ? rows only
      5
    
  • У объектов интерфейса Slice масса преимуществ, однако при работе с сайтами (и не только), нам очень часто бывает нужна т.н. пагинация - вывод необходимых элементов запроса на отдельной странице (страницах), при этом с возможностью подсчета количества выводимых страниц (их общего числа). В данном случае нам поможет интерфейс Page, наследник Slice, а значит он имеет все достоинства родителя и так же привносит свой функционал (см. DOC/SliceSortAndPageUsing/PageInterface.txt).

  • Применим интерфейс Page в методах *.findAllUserPagesBy() интерфейса FourthUserRepository и *.checkPaginationTest() тестового класса ForthUserRepositoryTest см. результаты запросов к БД:

      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        order by
          u1_0.id offset ? rows fetch first ? rows only
      Hibernate:
        select
          count(u1_0.id)
        from
          users u1_0
      
      1
      2
      
      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        order by
          u1_0.id offset ? rows fetch first ? rows only
      Hibernate:
        select
          count(u1_0.id)
        from
          users u1_0
      
      3
      4
      
      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        order by
          u1_0.id offset ? rows fetch first ? rows only
      
      5
    

Очень просто обнаружить дополнительный запрос с подсчетом - count, который в свою очередь мы можем также скорректировать при помощи @Query (см. FourthUserRepository.java метод *.findAllUserPagesWithCountBy()):

    @Query(value = "select u from User u",
           countQuery = "select count(distinct u.firstname) from User u")
    Page<User> findAllUserPagesWithCountBy(Pageable pageable);

В структуру кода тестового метода *.checkPaginationWithQueryCountTest() мы не вносим ни каких изменений в сравнении с *.checkPaginationTest() кроме использования аннотированного метода *.findAllUserPagesWithCountBy(Pageable pageable) и в консоли мы видим изменения дополнительных запросов к БД:

    select
      count(distinct u1_0.firstname)
    from
      users u1_0

Lesson 54 - Аннотация @EntityGraph в запроса репозиториев.

Тут мы рассмотрим вкратце методики применения аннотаций @EntityGraph и @NamedEntityGraph. Как бы мы не старались написать код приложения наиболее компактно, проблема N+1 при формировании запросов к БД остается. Поэтому, как и при изучении Hibernate Hibernate_part_7 по этому вопросу, мы прибегнем к помощи EntityGraph.

  • FifthUserRepository.java - репозиторий в котором мы применяем два способа работы с аннотацией @EntityGraph, это именованный @NamedEntityGraph (см. код и комментарии в User.java) - метод *.findAllUserWithNamedEntityGraphBy(), и атрибуты аннотации @EntityGraph - метод *.findAllUserWithAttributeEntityGraphBy();
  • FifthUserRepositoryTest.java - тестовый класс для изучения поведения обоих выше описанных метода репозитория;

!!! Внимание !!! При использовании сущностных графов @EntityGraph над методами репозитория, когда нам важна пагинация и подсчет страниц, мы получим проблемы чисто технического плана, поскольку будет происходить разделение запросов для получения LAZY сущностей (т.е. для одной Page получим перегруз данными, например для одной Company мы получим две локали и т.д.) и наше приложение либо может работать неправильно, либо посыпется при отображении (возврате) данных.

Всегда когда мы используем картирование OneToMany или ManyToMany и пытаемся воспользоваться Page нужно помнить о возможных проблемах с получением и отображением данных.


Lesson 55 - Аннотации @Lock и @QueryHints в запросах репозиториев.

Вопросы локирования (блокировки) транзакций подробно рассмотрены в Hibernate_part_8.

  • SixthUserRepository.java - демонстрационный репозиторий в котором мы применим аннотацию @Lock к методу List findTop3ByBirthDateBefore(LocalDate birthDate, Sort sort), в качестве параметров в аннотацию можно передать тип блокировки (см. описание DOC/LockModeAndQueryAnnotation/LockModeTypeEnum.txt). Применение различных типов блокировок более подробно рассмотрено в Hibernate_part_8.

!!! Помним, что !!! Оптимистические блокировки обрабатываются на уровне нашего приложения, в то время, как пессимистические блокировки обрабатываются (задаются) на уровне нашей БД, т.е. в наших запросах (в случае PostgreSQL: PESSIMISTIC_READ - select for share, PESSIMISTIC_WRITE - select for update).

  • SixthUserRepositoryTest.java - тестовый класс. Если мы применим к нашему методу параметр PESSIMISTIC_READ, то в консоли увидим запрос к БД с ключевым словосочетанием "for share":

      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        where
          u1_0.birth_date<?
        order by
          u1_0.lastname,
          u1_0.lastname fetch first ? rows only for share
    

Для того чтобы разобраться, какие блокировки относятся к 'for share' или 'for update' см. документацию к своей БД (PostgreSQL).

Пример применения аннотации @QueryHints приведен в том же тесте SixthUserRepository, краткая документация см. DOC/Hint.

См. док.:


Lesson 56 - Hibernate (проекция, DTO) Projection.

Для разъяснения понятия проекция (projection) можно изучить статьи из см. DOC/ArticleAboutProjection, понятие DTO - MVCPractice/DOC/DTO.txt и MVCPractice/DOC/DTO_example.png

  • PersonalInfo.java - простая DTO проекция, неизменяемая, т.к. нам нужно просто получить информацию из БД. Содержит имя, фамилию и дату рождения. Метод List findAllByCompanyId(Integer companyId) в ProjectionUserRepository.java;

  • PersonalRole.java - простая DTO проекция, созданная для демонстрации работы динамических проекций запросов, чуть отличается от предыдущей, описание принципа см. DOC/ArticleAboutProjection/SpringDataJPAProjections.txt. Метод List findAllByCompanyId(Integer companyId, Class type) в ProjectionUserRepository.java;

  • PersonalInfoTwo.java - проекция через интерфейс (см. открытые и закрытые проекции в DOC/ArticleAboutProjection/SpringDataJPAProjections.txt), в интерфейсе будут методы возвращающие необходимые нам значения полей сущностей через геттеры. Важным моментом в данном случае будет жесткое соответствие имен геттеров и имен полей сущностей в ResultSet.

  • UserRepositoryProjectionsTest.java - тестовый класс для демонстрации работы наших проекционных методов см. комментарии.


Lesson 57 - Spring Custom Repository Implementation - Самописные репозитории.

В статьях DOC/SpringDataJPATutorial/14_AddCustomMethodsToSingleRepository.txt и DOC/SpringDataJPATutorial/15_AddCustomMethodsToAllRepositories.txt вкратце описано, каким образом мы можем создавать и интегрировать наши собственные кастомные (самописные) репозитории и методы в наш проект.

Рассмотрим как это реализовано у нас:

  • Шаг 1. - Создадим UserFilterDto.java - некая проекция нашей сущности User;

  • Шаг 2. - Создадим интерфейс FilterUserRepository.java - свою кастомную имплементацию репозитория (для работы со всем доступным из JPA и Hibernate инструментарием), и создадим в нем метод (используем доступную из Spring-a именование по ключевым словам), принимающий параметр в качестве фильтра и возвращающий список User-ов подпадающих под переданный фильтр;

  • Шаг 3. - Создаем класс FilterUserRepositoryImpl.java, реализующий наш кастомный интерфейс и переопределяющий его метод;

  • Шаг 4. - Указываем Spring-у, что он должен использовать нашу рукописную имплементацию при обращении к UserRepository, для этого он должен расширять наш рукописный репозиторий, т.е.

    @Repository public interface UserRepository extends JpaRepository<User, Long>, FilterUserRepository { /* method code */ }

В данном случае Spring при обращении к UserRepository и методу *.findAllByFilter(UserFilterDto filter) будет искать имплементацию его родителя FilterUserRepository с постфиксом Impl (вот почему важно следовать декларации имен см. интерфейс EnableJpaRepositories метод *.repositoryImplementationPostfix()).

  • Шаг 5. - Напишем тест для нашего кастомного (самописного) метода - CustomUserRepositoryTest.java и проверим его работоспособность, см. вывод Hibernate запроса к БД на экран:

      Hibernate:
        select
          u1_0.id,
          u1_0.birth_date,
          u1_0.company_id,
          u1_0.firstname,
          u1_0.lastname,
          u1_0.role,
          u1_0.username
        from
          users u1_0
        where
          u1_0.lastname like ? escape ''
          and u1_0.birth_date<?
    

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


Lesson 58 - Spring JPA Auditing - Аудит работы нашего приложения.

Вопросы аудита наших записей в БД средствами Spring рассмотрен в статьях: DOC/SpringDataJPATutorial/13_1_AuditingSpringDataJPA.txt и DOC/SpringDataJPATutorial/13_2_AuditingSpringDataJPA.txt. Аудит средствами Hibernate рассмотрены в Hibernate_part_10. Но в сравнении с Hibernate, Spring очень сильно упростил нам жизнь.

Реализуем аудит в нашем приложении (будем отслеживать изменения в записях User):

  • Шаг 1. - Создадим аудирующую (фиксирующую изменения) сущность AuditingEntity.java, это абстрактный класс, все его наследники, смогут использовать его поля для фиксации изменений (создание и модификация, кем и когда), для этого:
    • Шаг 1.1 - Добавим поля класса Instant для когда (createdAt, modifiedAt) и для кто/кем (createdBy, modifiedBy) класса String, которые фиксируют соответствующие изменения в БД ;
    • Шаг 1.2 - Либо прямым кодом, либо через аннотации @Getter и @Setter, создаем геттеры и сеттеры для полей;
    • Шаг 1.3 - Для того чтобы наследники могли использовать поля родителя добавим аннотацию @MappedSuperclass;
    • Шаг 1.4 - Для того чтобы мы фиксировали изменения в сущностях нам нужны слушатели (listeners), поэтому добавляем аннотацию @EntityListeners(AuditingEntityListener) в параметры которой передаем, не наш самописный слушатель, а уже готовый Spring-овый слушатель AuditingEntityListener;
    • Шаг 1.5 - Указываем Spring-овому слушателю (listener-у) какие поля нужно обновлять, аннотируя их соответствующими аннотациями (@CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy) см. AuditingEntity.java;
  • Шаг 2. - Поскольку наш класс аудита реализует нашу же BaseEntity, мы легко можем изменить класс User. Поскольку мы запланировали вести аудит его изменений, унаследуемся от AuditingEntity. И конечно, дабы избежать исключений добавим еще одно поле в аннотацию @EqualsAndHashCode(callSuper=false) в сущности User;
  • Шаг 3. - Внесем соответствующие изменения в БД, добавим поля, которые будут фиксировать все изменения в записях сущностей User (см. SQL скрипты DOC/SQL_Update_Base_Table/UserTableUpdate.sql);
  • Шаг 4. - Запускаем механизм аудирования изменений данных в БД. Для этого мы должны использовать аннотацию @EnableJpaAuditing, которой можем аннотировать, например основной класс нашего приложения SpringAppRunner.java (но мы этого делать не будем), либо создать свой класс конфигурации аудита и пометить данной аннотацией его, что и сделаем. Создаем AuditConfiguration.java и естественно аннотируем его еще и как @Configuration.
  • Шаг 5. - Создаем метод провайдер данных *.auditorAware() для наших полей в классе конфигурации аудита AuditConfiguration, т.е. мы должны предоставить Spring-у BEAN интерфейса AuditorAware;
  • Шаг 6. - Создаем тестовый класс UserAuditingTest.java и проверяем работу аудита;

См. док.:


Lesson 59 - Hibernate-Envers в Spring приложении.

Как было описано выше, для данного раздела, подключим Spring зависимость, чтобы использовать механизм Envers:

    implementation 'org.springframework.data:spring-data-envers'

Для сравнения с Hibernate функционалом и возможностями см. Hibernate_part_10 и оф. документация (ENG) Hibernate ORM Envers или Hibernate Envers

Освежим в памяти, для чего нужен функционал Envers:

  • Аудит всех сопоставлений, определенных спецификацией JPA.
  • Аудит некоторых сопоставлений Hibernate, которые расширяют JPA, например, пользовательские типы и коллекции/карты «простых» типов, таких как строки, целые числа.
  • Регистрация данных для каждой ревизии с использованием «объекта ревизии».
  • Запрос исторических снимков объекта и его ассоциаций.

Для работы с механизмом отслеживания версий (ревизий) наших сущностей проделаем следующие шаги:

  • Шаг 1. - Создадим отслеживающую изменения версий сущность (некий, Git-подобный механизм) Revision.java, содержащих два поля: id - номер проведенного изменения и timestamp - время, когда было проведено изменение.

  • Шаг 2. - Над всеми сущностями, которые мы планируем отслеживать, мы должны проставить аннотацию @Audited, например, наша сущность User см. User.java.

  • Шаг 3. - Настраиваем (задаем) параметры отслеживания. В случае нашей сущности User мы не хотим отслеживать связные сущности, например Company, а так же любые коллекции внутри User. Поэтому в аннотацию @Audited передаем параметр targetAuditMode = RelationTargetAuditMode.NOT_AUDITED, а коллекцию List userChats - помечаем, как @NotAudited.

  • Шаг 4. - Необходимо создать в нашей БД соответствующие таблицы, которые будут отслеживать изменения аудируемой сущности. Настроим данный функционал на автоматическое создание. Поскольку мы будем проверять работоспособность кода в наших тестах, внесем необходимые изменения в resources/application-test.yml:

      spring:
        jpa:
          properties.hibernate:
            hbm2ddl.auto: update
    

В данном случае при запуске теста, который вносит изменения в отслеживаемую сущность User, в нашей БД автоматически будет создано две таблицы: revision и users_aud. В них будет внесена информация об изменениях сущности см. БД.

  • Шаг 5. - Запустим механизм аудирования. Для этого в файле авто-конфигурации нашего приложения AuditConfiguration.java добавим аннотацию @EnableEnversRepositories или см. DOC/DataEnvers/EnableEnversRepositories.txt. Передадим в параметры аннотации наш корневой пакет для сканирования отслеживаемых объектов: basePackageClasses = SpringAppRunner.class
  • Шаг 6. - Для наглядности работы механизма Hibernate Envers, мы запустим тест и внесем изменения в БД, чтобы изменения были зафиксированы необходимо аннотировать тестовый метод, как @Commit (см. UserAuditingTest.java).

Фактически, реализовав данные шаги мы получим в нашей БД необходимые таблицы для каждой отслеживаемой сущности (у нас это User). Естественно мы не только фиксируем изменения, но и можем к ним обращаться, а так же откатывать внесенные изменения. Для этого мы не будем создавать новую сущность, а просто расширим наш уже существующий UserRepository, добавив еще один интерфейс - RevisionRepository, как расширяемый. И тогда нам будут доступны все методы данного интерфейса для работы с внесенными изменениями, т.е. с ревизиями см. DOC/DataEnvers/RevisionRepositoryInterface.txt.

  • FindRevisionTest.java - тестовый класс для проверки работы методов полученных UserRepository при расширении RevisionRepository см. тестовый метод checkFindLastRevisionTest().

См. док.:


См. официальные Guides:

  • Getting Started Guides - Эти руководства, рассчитанные на 15–30 минут, содержат быстрые практические инструкции по созданию «Hello World» для любой задачи разработки с помощью Spring. В большинстве случаев единственными необходимыми требованиями являются JDK и текстовый редактор.
  • Topical Guides - Тематические руководства предназначенные для прочтения и понимания за час или меньше, содержит более широкий или субъективный контент, чем руководство по началу работы.
  • Tutorials - Эти учебники, рассчитанные на 2–3 часа, обеспечивают более глубокое контекстное изучение тем разработки корпоративных приложений, что позволяет вам подготовиться к внедрению реальных решений.


Рекомендованные для изучения материалы не вошедшие в папку DOC: