Spring Data JPA

    В статье опишу использование Spring Data.

    Spring Data — дополнительный удобный механизм для взаимодействия с сущностями базы данных, организации их в репозитории, извлечение данных, изменение, в каких то случаях для этого будет достаточно объявить интерфейс и метод в нем, без имплементации.

    Содержание:

    1. Spring Repository
    2. Методы запросов из имени метода
    3. Конфигурация и настройка
    4. Специальная обработка параметров
    5. Пользовательские реализации для репозитория
    6. Пользовательский Базовый Репозиторий
    7. Методы запросов — Query


    1. Spring Repository


    Основное понятие в Spring Data — это репозиторий. Это несколько интерфейсов которые используют JPA Entity для взаимодействия с ней. Так например интерфейс
    public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID>
    обеспечивает основные операции по поиску, сохранения, удалению данных (CRUD операции)

    T save(T entity);
    Optional findById(ID primaryKey);
    void delete(T entity);
    

    и др. операции.

    Есть и другие абстракции, например PagingAndSortingRepository.

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

    1. Создаем сущность

    @Entity
    @Table(name = "EMPLOYEES")
    public class Employees {
        private Long employeeId;
        private String firstName;
        private String lastName;
        private String email;
        // . . . 
    

    2. Наследоваться от одного из интерфейсов Spring Data, например от CrudRepository

    @Repository
    public interface CustomizedEmployeesCrudRepository extends CrudRepository<Employees, Long>
    

    3. Использовать в клиенте (сервисе) новый интерфейс для операций с данными

    @Service
    public class EmployeesDataService {
    
     @Autowired
     private CustomizedEmployeesCrudRepository employeesCrudRepository;
    
      @Transactional
      public void testEmployeesCrudRepository() {
    	Optional<Employees> employeesOptional = employeesCrudRepository.findById(127L);
    	//....
        }	

    Здесь я воспользовался готовым методом findById. Т.е. вот так легко и быстро, без имплементации, получим готовый перечень операций из CrudRepository:

        S save(S var1);
        Iterable<S> saveAll(Iterable<S> var1);
        Optional<T> findById(ID var1);
        boolean existsById(ID var1);
        Iterable<T> findAll();
        Iterable<T> findAllById(Iterable<ID> var1);
        long count();
        void deleteById(ID var1);
        void delete(T var1);
        void deleteAll(Iterable<? extends T> var1);
        void deleteAll();
    

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

    2. Методы запросов из имени метода


    Запросы к сущности можно строить прямо из имени метода. Для этого используется механизм префиксов find…By, read…By, query…By, count…By, и get…By, далее от префикса метода начинает разбор остальной части. Вводное предложение может содержать дополнительные выражения, например, Distinct. Далее первый By действует как разделитель, чтобы указать начало фактических критериев. Можно определить условия для свойств сущностей и объединить их с помощью And и Or. Примеры

    @Repository
    public interface CustomizedEmployeesCrudRepository extends CrudRepository<Employees, Long> {
        // искать по полям firstName And LastName
        Optional<Employees> findByFirstNameAndLastName(String firstName, String lastName);
        // найти первые 5 по FirstName начинающихся с символов и сортировать по FirstName 
        List<Employees> findFirst5ByFirstNameStartsWithOrderByFirstName(String firstNameStartsWith);
    

    В документации определен весь перечень, и правила написания метода. В качестве результата могут быть сущность T, Optional, List, Stream. В среде разработки, например в Idea, есть подсказка для написания методов запросов.

    image
    Достаточно только определить подобным образом метод, без имплементации и Spring подготовит запрос к сущности.

    	
    @SpringBootTest
    public class DemoSpringDataApplicationTests {
    @Autowired
    private CustomizedEmployeesCrudRepository employeesCrudRepository;
    
    @Test
    @Transactional
    public void testFindByFirstNameAndLastName() {
    		Optional<Employees> employeesOptional = employeesCrudRepository.findByFirstNameAndLastName("Alex", "Ivanov");
    

    3. Конфигурация и настройка


    Весь проект доступен на github
    github DemoSpringData

    Здесь лишь коснусь некоторых особенностей.

    В context.xml определенны бины transactionManager, dataSource и entityManagerFactory. Важно указать в нем также

    <jpa:repositories base-package="com.example.demoSpringData.repositories"/>   
    

    путь где определены репозитории.

    EntityManagerFactory настроен на работу с Hibernate ORM, а он в свою очередь с БД Oracle XE, тут возможны и другие варианты, в context.xml все это видно. В pom файле есть все зависимости.

    4. Специальная обработка параметров


    В методах запросов, в их параметрах можно использовать специальные параметры Pageable, Sort, а также ограничения Top и First.

    Например вот так можно взять вторую страницу (индекс с -0), размером в три элемента и сортировкой по firstName, предварительно указав в методе репозитория параметр Pageable, также будут использованы критерии из имени метода — «Искать по FirstName начиная с % „

    @Repository
    public interface CustomizedEmployeesCrudRepository extends CrudRepository<Employees, Long> {
        List<Employees> findByFirstNameStartsWith(String firstNameStartsWith, Pageable page);
    //....
    }
    // пример вызова
    @Test
    @Transactional
    public void testFindByFirstNameStartsWithOrderByFirstNamePage() {
    	List<Employees> list = employeesCrudRepository
    	.findByFirstNameStartsWith("A", PageRequest.of(1,3, Sort.by("firstName")));
    	list.forEach(e -> System.out.println(e.getFirstName() + " " +e.getLastName()));
    }
    

    5. Пользовательские реализации для репозитория


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

    Объявляю интерфейс

    public interface CustomizedEmployees<T> {
    
        List<T> getEmployeesMaxSalary();
    
    }
    

    Имплементирую интерфейс. С помощью HQL (SQL) получаю сотрудников с максимальной оплатой, возможны и другие реализации.

    public class CustomizedEmployeesImpl implements CustomizedEmployees {
    
        @PersistenceContext
        private EntityManager em;
    
        @Override
        public List getEmployeesMaxSalary() {
            return em.createQuery("from Employees where salary = (select max(salary) from Employees )", Employees.class)
                    .getResultList();
        }
    }
    

    А также расширяю Crud Repository Employees еще и CustomizedEmployees.

    @Repository
    public interface CustomizedEmployeesCrudRepository extends CrudRepository<Employees, Long>, CustomizedEmployees<Employees> 
    

    Здесь есть одна важная особенность. Класс имплементирующий интерфейс, должен заканчиваться (postfix) на Impl, или в конфигурации надо поставить свой postfix

    <repositories base-package="com.repository" repository-impl-postfix="MyPostfix" /> 

    Проверяем работу этого метода через репозиторий

    public class DemoSpringDataApplicationTests {
    @Autowired
     private CustomizedEmployeesCrudRepository employeesCrudRepository;
    
    @Test
    @Transactional
    public void testMaxSalaryEmployees() {
    	List<Employees> employees = employeesCrudRepository.getEmployeesMaxSalary();
    	employees.stream()
    	.forEach(e -> System.out.println(e.getFirstName() + " " + e.getLastName() + " " + e.getSalary()));
    }
    

    Другой случай, когда надо изменить поведение уже существующего метода в интерфейсе Spring, например delete в CrudRepository, мне надо что бы вместо удаления из БД, выставлялся признак удаления. Техника точно такая же. Ниже пример:

    public interface CustomizedEmployees<T> {
    
        void delete(T entity);
        // ...
    }
    // Имплементация CustomizedEmployees
    public class CustomizedEmployeesImpl implements CustomizedEmployees {
    
        @PersistenceContext
        private EntityManager em;
    
        @Transactional
        @Override
        public void delete(Object entity) {
            Employees employees = (Employees) entity;
            employees.setDeleted(true);
            em.persist(employees);
        }
    
    

    Теперь если в employeesCrudRepository вызвать delete, то объект будет только помечен как удаленный.

    6. Пользовательский Базовый Репозиторий


    В предыдущем примере я показал как переопределить delete в Crud репозитории сущности, но если это надо делать для всех сущностей проекта, делать для каждой свой интерфейс как то не очень..., тогда в Spring data можно настроить свой базовый репозиторий. Для этого:
    Объявляется интерфейс и в нем метод для переопределения (или общий для всех сущностей проекта). Тут я еще для всех своих сущностей ввел свой интерфейс BaseEntity (это не обязательно), для удобства вызова общих методов, его методы совпадают с методами сущности.

    public interface BaseEntity {
    
        Boolean getDeleted();
        void setDeleted(Boolean deleted);
    }
    
    // Сущность Employees 
    @Entity
    @Table(name = "EMPLOYEES")
    public class Employees implements BaseEntity {
      private Boolean deleted;
      
        @Override
        public Boolean getDeleted() {
            return deleted;
        }
    
        @Override
        public void setDeleted(Boolean deleted) {
            this.deleted = deleted;
        }
    
    
    // Базовый пользовательский интерфейс
    @NoRepositoryBean
    public interface BaseRepository <T extends BaseEntity, ID extends Serializable>
            extends JpaRepository<T, ID> {
    
        void delete(T entity);
    }
    
    //Базовый пользовательский класс имплементирующий BaseRepository
    public class BaseRepositoryImpl <T extends BaseEntity, ID extends Serializable>
            extends SimpleJpaRepository<T, ID>
            implements BaseRepository<T, ID> {
    
        private final EntityManager entityManager;
    
        public BaseRepositoryImpl(JpaEntityInformation<T, ?> entityInformation,
                         EntityManager entityManager) {
            super(entityInformation, entityManager);
            this.entityManager = entityManager;
        }
    
        @Transactional
        @Override
        public void delete(BaseEntity entity) {
            entity.setDeleted(true);
            entityManager.persist(entity);
        }
    }
    
    

    В конфигурации надо указать этот базовый репозиторий, он будет общий для всех репозиториев проекта

        <jpa:repositories base-package="com.example.demoSpringData.repositories"
                          base-class="com.example.demoSpringData.BaseRepositoryImpl"/>
    

    Теперь Employees Repository (и др.) надо расширять от BaseRepository и уже его использовать в клиенте.

    public interface EmployeesBaseRepository extends BaseRepository <Employees, Long> {
      // ...
    }
    

    Проверяю работу EmployeesBaseRepository

    public class DemoSpringDataApplicationTests {
     @Resource
      private EmployeesBaseRepository employeesBaseRepository;
    
    @Test
    @Transactional
    @Commit
    public void testBaseRepository() {
    	Employees employees = new Employees();
    	employees.setLastName("Ivanov");
            // Query by Example  (QBE)
    	Example<Employees> example = Example.of(employees);
    	Optional<Employees> employeesOptional = employeesBaseRepository.findOne(example);
    	employeesOptional.ifPresent(employeesBaseRepository::delete);
    }
    

    Теперь также как и ранее, объект будет помечен как удаленный, и это будет выполняться для всех сущностей, которые расширяют интерфейс BaseRepository. В примере был применен метод поиска — Query by Example (QBE), я не буду здесь его описывать, из примера видно что он делает, просто и удобно.

    7. Методы запросов — Query


    Ранее я писал, что если нужен специфичный метод или его реализация, которую нельзя описать через имя метода, то это можно сделать через некоторый Customized интерфейс ( CustomizedEmployees) и сделать реализацию вычисления. А можно пойти другим путем, через указание запроса (HQL или SQL), как вычислить данную функцию.
    Для моего примера c getEmployeesMaxSalary, этот вариант реализации даже проще. Я еще усложню его входным параметром salary. Т.е. достаточно объявить в интерфейсе метод и запрос вычисления.

    @Repository
    public interface CustomizedEmployeesCrudRepository extends CrudRepository<Employees, Long>, CustomizedEmployees<Employees> {
    
        @Query("select e from Employees e where e.salary > :salary")
        List<Employees> findEmployeesWithMoreThanSalary(@Param("salary") Long salary, Sort sort);
        // ...
    }
    

    Проверяю

    @Test
    @Transactional
    public void testFindEmployeesWithMoreThanSalary() {
    	List<Employees> employees = employeesCrudRepository.findEmployeesWithMoreThanSalary(10000L, Sort.by("lastName"));
    

    Упомяну лишь еще, что запросы могут быть и модифицирующие, для этого к ним добавляется еще аннотация @Modifying

    @Modifying
    @Query("update Employees e set e.firstName = ?1 where e.employeeId = ?2")
    int setFirstnameFor(String firstName, String employeeId);
    

    Еще одной из замечательных возможностей Query аннотации — это подстановка типа домена сущности в запрос по шаблону #{#entityName}, через SpEL выражения.

    Так например в моем гипотетическом примере, когда мне надо для всех сущностей иметь признак “удален», я сделаю базовый интерфейс с методом получения списка объектов с признаком «удален» или «активный»

    @NoRepositoryBean
    public interface ParentEntityRepository<T> extends Repository<T, Long> {
    
        @Query("select t from #{#entityName} t where t.deleted = ?1")
        List<T> findMarked(Boolean deleted);
    }
    

    Далее все репозитории для сущностей можно расширять от него. Интерфейсы которые не являются репозиториями, но находятся в «base-package» папке конфигурации, надо аннотировать @NoRepositoryBean.

    Репозиторий Employees

    @Repository
    public interface EmployeesEntityRepository extends ParentEntityRepository <Employees> {
    }
    

    Теперь когда будет выполняться запрос, в тело запроса будет подставлено имя сущности T для конкретного репозитория который будет расширять ParentEntityRepository, в данном случае Employees.

    Проверка

    @SpringBootTest
    public class DemoSpringDataApplicationTests {
    @Autowired
    private EmployeesEntityRepository employeesEntityRepository;
    
    @Test
    @Transactional
    public void testEntityName() {
    	List<Employees> employeesMarked = employeesEntityRepository.findMarked(true);
            // ...
    

    Материалы
    Spring Data JPA — Reference Documentation
    Проект на github.
    Поделиться публикацией

    Комментарии 12

      0

      Все это хорошо пока не дойдешь до динамических запросов с несколькими параметрами, на что есть два решения: QueryDSL и JPA Specifications.
      QueryDSL, как я понял, малопопулярен. Но во обоих случаях придется писать довольно много кода, что сравнимо с Criteria API. Тогда зачем еще один уровень абстракции, если можно это сделать со старым, добрым Criteria API? Что я и стал пока делать.


      Может скажет, что "JPA Specifications" все-таки правильный путь?
      Статья Using Spring Data JPA Specification как-то малоубедительна.

        0
        Вовсе не исключаю возможности использования Criteria Api, но для массовых простых операция декларативный подход очень даже.
          0

          Используем QueryDSL, довольны.

            0
            Тут вопрос о не исключительность Sprint data, а как о некоем фасаде между сущностью и прикладной логикой, а внутри репозитория думаю найдется место и Criteria
              0
              Мне в большинстве случаев хватало native query.
              Из-за этого кончено репозиторий становился сильнее привязан к конкретной БД.
              Зато не надо ломать голову с Criteria API. Более не удобного способа работы с БД я еще не встречал :-)
                0
                В Criteria был сделан упор на типизацию, на выявление ошибок еще на этапе компиляции, это видимо сделало его несколько сложным
                  0
                  ИМХО просто «странно» спроектированная «фигня».
                  Как минимум в PostgreSQL начиная с 8 версии типизация строгая.
                  Помню, когда переходил с 7-ой на 8-ю это доставило некоторое количество WTF-моментов
                  0
                  Каким образом native query помогает решить проблему «динамических запросов с несколькими параметрами»?
                    0
                    Тем, что не надо писать динамические запросы с несколькими параметрами. ;-)
                    Точнее, можно оформить в виде репозитария несколько запросов с разными параметрами, собрав их из нескольких SQL-кусков.
                  +1
                  Все это хорошо пока не дойдешь до динамических запросов с несколькими параметрами

                  Основная проблема в том, что мы хотим по-возможности иметь type-safe queries. А все эти искусственные findBy*() или Query не решают проблемы. Интерфейс Repository начинает мешать, когда число сущностей становится несколько десятков, а модель данных не такая тривиальная. Кроме того, эстетически findFirst5ByFirstNameStartsWithOrderByFirstName() читается в десять раз хуже, чем обычный хорошо оформленный JPQL-запрос, особенно если для этого используется какой-нибудь QueryDSL или Criteria API.


                  QueryDSL, как я понял, малопопулярен

                  Очень даже популярен, до сих пор живет и релизится. Правда не так активно. Criteria API уродливый и жутко неудобный (как впрочем и большинство стандартизированных API). Могу порекомендовать замечательную библиотеку Jinq, где критерии задаются ввиде обычных джавовских лямбд. Не надо кодогенерации и искусственного DSL.


                  Тогда зачем еще один уровень абстракции

                  А это политика Spring-а — влезть посредником во все, что только возможно, там где нужно и ненужно, чтобы пользователи вообще не мыслили разработку без спринга.

                    0

                    Спасибо за Jinq, вижу стоит попробовать.

                      0
                      Ничего не мешает. :-)
                      Пишешь native query и получаешь удобство декларативного SQL.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое