В статье опишу использование 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)


  1. zesetup
    07.01.2019 14:15

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


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


    1. arylkov Автор
      07.01.2019 17:39

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


    1. ruslanys
      07.01.2019 20:52

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


    1. arylkov Автор
      07.01.2019 21:01

      Тут вопрос о не исключительность Sprint data, а как о некоем фасаде между сущностью и прикладной логикой, а внутри репозитория думаю найдется место и Criteria


    1. mad_nazgul
      08.01.2019 07:40

      Мне в большинстве случаев хватало native query.
      Из-за этого кончено репозиторий становился сильнее привязан к конкретной БД.
      Зато не надо ломать голову с Criteria API. Более не удобного способа работы с БД я еще не встречал :-)


      1. arylkov Автор
        08.01.2019 08:12

        В Criteria был сделан упор на типизацию, на выявление ошибок еще на этапе компиляции, это видимо сделало его несколько сложным


        1. mad_nazgul
          09.01.2019 08:21

          ИМХО просто «странно» спроектированная «фигня».
          Как минимум в PostgreSQL начиная с 8 версии типизация строгая.
          Помню, когда переходил с 7-ой на 8-ю это доставило некоторое количество WTF-моментов


      1. ris58h
        08.01.2019 12:03

        Каким образом native query помогает решить проблему «динамических запросов с несколькими параметрами»?


        1. mad_nazgul
          09.01.2019 08:28

          Тем, что не надо писать динамические запросы с несколькими параметрами. ;-)
          Точнее, можно оформить в виде репозитария несколько запросов с разными параметрами, собрав их из нескольких SQL-кусков.


    1. Throwable
      08.01.2019 18:43

      Все это хорошо пока не дойдешь до динамических запросов с несколькими параметрами

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


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

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


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

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


      1. zesetup
        08.01.2019 23:08

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


      1. mad_nazgul
        09.01.2019 08:33

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