Перейти к содержанию

Spring Data

Spring Data - проект экосистемы Spring, который убирает шаблонный код доступа к данным: реляционные базы, NoSQL, Redis. Главная идея - репозиторий-интерфейс, реализацию которого Spring генерирует сам.

Spring Data - не ORM. Он даёт удобный интерфейс поверх ORM. Сама работа с объектами и SQL - у JPA-провайдера, по умолчанию Hibernate.


Spring Data JPA и Hibernate

Три слоя, которые часто путают:

  • JPA - спецификация: как отображать объекты на таблицы и управлять их состоянием. Аннотации @Entity, @Table, @Id, EntityManager
  • Hibernate - реализация JPA: генерирует SQL, держит persistence context, кэширует (см. Hibernate)
  • Spring Data JPA - часть Spring Data поверх JPA: репозитории без реализации, запросы по имени метода, пагинация

spring-boot-starter-data-jpa - стартер, который подключает всё сразу: Spring Data JPA, Hibernate как реализацию JPA, пул соединений HikariCP. Если Hibernate в classpath, Boot сам настраивает его через автоконфигурацию: DataSource, EntityManagerFactory, менеджер транзакций.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Настройка подключения

spring.datasource.url=jdbc:postgresql://localhost:5432/your_database_name
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=org.postgresql.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
  • spring.datasource.url - JDBC-адрес базы
  • spring.datasource.username, password - учётные данные
  • spring.datasource.driver-class-name - класс драйвера. Boot обычно определяет его по URL сам
  • spring.jpa.hibernate.ddl-auto - что Hibernate делает со схемой при старте
  • spring.jpa.show-sql - печатать SQL в консоль, удобно для отладки
  • spring.jpa.properties.hibernate.format_sql - печатать SQL с отступами

ddl-auto - режимы управления схемой:

Значение Что делает
none Ничего
validate Проверяет, что схема совпадает с сущностями, иначе падает при старте
update Добавляет новые таблицы и столбцы. Ничего не удаляет и не переименовывает
create Пересоздаёт схему при старте
create-drop Пересоздаёт при старте и удаляет при остановке. Для тестов

Warning

update удобен на учебном проекте, но на проде схему ведут миграциями (Flyway, Liquibase), а Hibernate ставят в validate или none.

Note

В исходной заметке был spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect. В Hibernate 6 (Spring Boot 3) диалект определяется по подключению сам, и это свойство обычно убирают, иначе Hibernate пишет предупреждение.


Репозитории

Репозиторий - интерфейс доступа к сущности. Реализацию генерирует Spring Data при старте.

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<Employee> findByLastName(String lastName);              // запрос по имени метода

    @Query("select e from Employee e where e.salary > :min")      // свой JPQL
    List<Employee> findRich(@Param("min") BigDecimal min);
}
  • JpaRepository<T, ID> даёт CRUD, пагинацию и сортировку: save, findById, findAll, delete
  • методы по имени (findByLastName) превращаются в JPQL-запрос
  • @Query - свой JPQL, или нативный SQL с nativeQuery = true
  • реализация по умолчанию - SimpleJpaRepository, её методы уже @Transactional

Как проходит вызов employeeRepository.findAll():

  1. Прокси репозитория передаёт вызов в SimpleJpaRepository.
  2. Spring Data строит JPQL или Criteria-запрос.
  3. Hibernate переводит его в SQL под конкретную СУБД и выполняет через JDBC.
  4. Hibernate превращает строки результата в объекты Employee и кладёт их в persistence context.

Слои типового приложения: сущности (@Entity) → репозитории (JpaRepository) → сервисы → контроллеры, которые принимают HTTP-запросы и зовут сервисы.


Запросы в репозиториях

findById и Optional. findById() возвращает Optional<T>: записи может не быть, и вызывающий код обязан это обработать (см. Запросы в DAO и Optional).

JOIN FETCH в @Query - решает N+1: дети загружаются в том же запросе. Своим методом или переопределив стандартный:

public interface ClientRepository extends JpaRepository<Client, Integer> {

    @Query("select c from Client c join fetch c.payments")
    List<Client> findAllWithPayments();   // свой метод

    @Override
    @Query("select c from Client c join fetch c.payments")
    List<Client> findAll();               // переопределённый стандартный
}

@EntityGraph - план загрузки прямо на методе репозитория, без JPQL:

@EntityGraph(attributePaths = {"coordinates", "admin"})   // загрузить связи сразу
List<Vehicle> findByType(VehicleType type);

См. N+1 и планы загрузки.

@Modifying - массовый UPDATE или DELETE через @Query. Без неё Spring Data считает запрос выборкой.

@Modifying(clearAutomatically = true)   // очистить persistence context после запроса
@Transactional
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
int deactivateInactiveUsers(@Param("date") LocalDate date);   // вернёт число изменённых строк
  • запрос идёт мимо persistence context: загруженные раньше сущности останутся со старыми значениями. clearAutomatically = true очищает контекст после запроса
  • нужна транзакция: @Transactional на методе или вызов из транзакционного сервиса, иначе TransactionRequiredException. Свои методы репозитория транзакцию по умолчанию не получают

@Lock - пессимистичная блокировка на методе репозитория (см. Pessimistic locking).


Аудит сущностей

Аудит - Spring Data сам заполняет поля «кто и когда создал или изменил запись».

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")   // включает аудит, указывает бин текущего пользователя
public class JpaConfig {
    @Bean
    public AuditorAware<Integer> auditorAware() {
        return new ApplicationAuditAware();               // берёт id пользователя из SecurityContext
    }
}

@Entity
@EntityListeners(AuditingEntityListener.class)         // без слушателя поля не заполнятся
public class Token {
    @CreatedDate        private Instant createdAt;
    @LastModifiedDate   private Instant updatedAt;
    @CreatedBy          private Integer createdBy;
    @LastModifiedBy     private Integer updatedBy;
}
  • @CreatedDate, @LastModifiedDate - время создания и изменения
  • @CreatedBy, @LastModifiedBy - кто создал и изменил, значение берётся из AuditorAware

TODO

  • Создание модели данных: сущности, связи, пример Employee