Логотип Workflow

Article

Spring Data Jpa Basics

Этап 5 — Spring Data JPA: Entity, таблицы, SQL и транзакции

Spring Data JPA нужен для работы с базой данных через Java-объекты. Вместо того чтобы в каждом методе вручную писать insert into, select, update и разбирать строки результата, разработчик описывает Java-класс как сущность, связывает его с таблицей и использует repository. SQL всё равно существует, но большую часть типового SQL генерирует Hibernate.

Новичку важно не начинать с Persistence Context и LazyInitializationException. Сначала нужно понять более простую цепочку: таблица в базе хранит строки, Java Entity представляет одну строку этой таблицы, JPA-аннотации описывают соответствие между классом и таблицей, Hibernate читает это соответствие и выполняет SQL, а Spring Data JPA даёт удобный repository API.

JPA entity to table mapping

Что такое Entity и как она мапится в SQL

Entity — это Java-класс, который соответствует таблице в базе данных. Объект entity обычно соответствует одной строке таблицы. Поля entity соответствуют колонкам. Такое соответствие называют mapping: мы объясняем Hibernate, какой класс хранится в какой таблице и какое поле связано с какой колонкой.

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "status", nullable = false)
    private String status;

    @Column(name = "total_amount", nullable = false)
    private BigDecimal totalAmount;
}

@Entity говорит: этот класс является сущностью JPA. @Table(name = "orders") говорит: объекты этого класса хранятся в таблице orders. @Id отмечает primary key. @GeneratedValue говорит, что значение id создаёт база или provider. @Column задаёт имя колонки и простые ограничения.

Такой класс соответствует SQL-таблице примерно такого вида:

create table orders (
    id bigserial primary key,
    status varchar(255) not null,
    total_amount numeric(19, 2) not null
);

Если вызвать orderRepository.save(order), разработчик не пишет insert вручную. Hibernate смотрит на mapping и готовит SQL сам. Для новой сущности это будет insert, для изменённой managed-сущности — update, для поиска по id — select. Важно: SQL не исчезает. Он просто создаётся на основе Java-классов и JPA-аннотаций.

JPA, Hibernate и Spring Data: кто за что отвечает

JPA — это спецификация, то есть набор правил и аннотаций для ORM в Java. ORM означает Object-Relational Mapping: связь между объектной моделью Java и реляционной моделью базы данных. Слово «контракт» в этом контексте означает договорённость API: какие аннотации есть, что значит @Entity, как работает EntityManager, какие состояния может иметь сущность. JPA сама по себе не является конкретной библиотекой, которая выполняет SQL в вашем приложении.

Hibernate — это реализация JPA. Он читает аннотации JPA, хранит информацию о mapping, создаёт SQL, отправляет запросы в базу через JDBC, получает строки результата и превращает их обратно в Java-объекты. Когда в Spring Boot проекте говорят «JPA сохранила сущность», на практике часто именно Hibernate выполнил основную работу.

Spring Data JPA — это слой удобства поверх JPA/Hibernate. Он даёт repository interfaces, например JpaRepository<Order, Long>, чтобы не писать одинаковый код для findById, save, delete, pagination и простых запросов.

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatus(String status);
}

Метод findByStatus не имеет тела, но Spring Data понимает имя метода и строит запрос. Для таблицы orders это будет логически похоже на select * from orders where status = ?. Для сложных случаев можно писать JPQL или native SQL явно, но начинать обычно стоит с готовых repository-возможностей.

УровеньПростое объяснениеПример
JPAПравила и аннотации для связи объектов с таблицами@Entity, @Table, @Id
HibernateБиблиотека, которая реализует эти правила и выполняет SQLГенерирует select, insert, update
Spring Data JPAУдобные repository interfaces поверх JPAJpaRepository, findByStatus
PostgreSQL/MySQLРеальная база данных, где лежат таблицы и строкиТаблица orders

Как использовать repository в сервисе

Контроллер не должен напрямую решать, как сохранять заказ. Обычно controller принимает HTTP-запрос, service выполняет бизнес-сценарий, repository работает с базой. Это сохраняет границы ответственности.

@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public Order createOrder(BigDecimal totalAmount) {
        Order order = new Order();
        order.setStatus("NEW");
        order.setTotalAmount(totalAmount);
        return orderRepository.save(order);
    }
}

Когда вызывается save, Spring Data JPA передаёт работу JPA provider-у, обычно Hibernate. Hibernate понимает, что объект новый, и готовит insert into orders (...) values (...). Если включить SQL logging, можно увидеть реальные запросы, которые уходят в базу. Это полезно, потому что repository скрывает шаблонный код, но не должен скрывать от разработчика понимание происходящего.

Что такое @Transactional простыми словами

Транзакция — это граница одной целостной операции с базой данных. Если операция успешна, изменения фиксируются через commit. Если произошла ошибка, изменения откатываются через rollback. Это нужно, чтобы база не осталась в промежуточном состоянии.

Представь перевод денег: нужно списать сумму с одного счёта и добавить на другой. Если списание прошло, а добавление упало, данные испорчены. Транзакция делает эти шаги одной операцией: либо всё успешно, либо всё откатывается.

@Transactional в Spring ставят чаще всего на service method, потому что именно сервис описывает бизнес-операцию целиком. Эта аннотация не декоративная: Spring создаёт вокруг метода transaction boundary. До входа в метод открывается транзакция, внутри метода выполняется работа с repository, при успешном завершении выполняется commit, при runtime exception обычно выполняется rollback.

@Transactional
public void payOrder(Long orderId) {
    Order order = orderRepository.findById(orderId)
        .orElseThrow(() -> new OrderNotFoundException(orderId));

    order.setStatus("PAID");
}

В примере нет явного orderRepository.save(order) после изменения статуса. Если Order был загружен внутри транзакции, Hibernate следит за ним. При commit он увидит, что поле status изменилось, и выполнит update orders set status = ? where id = ?. Это называется dirty checking.

Жизненный цикл сущности и Persistence Context

Теперь можно объяснить Persistence Context. Persistence Context — это рабочая область Hibernate внутри транзакции, где он хранит загруженные entity-объекты и следит за их изменениями. Пока сущность находится в этой области, она называется managed. Managed означает: Hibernate знает этот объект и может синхронизировать его с базой.

Если объект только создан через new Order(), но ещё не сохранён, он transient. Если он сохранён или загружен внутри транзакции, он managed. Если транзакция закончилась и объект больше не связан с Hibernate session, он detached. Detached-объект остаётся обычным Java-объектом, но Hibernate уже не следит за его изменениями автоматически.

JPA persistence context

Flush — это момент, когда Hibernate синхронизирует изменения из Persistence Context с базой через SQL. Обычно flush происходит перед commit или перед некоторыми запросами. Поэтому SQL может появиться не ровно в строке setStatus, а позже, когда Hibernate решит отправить изменения.

Lazy vs Eager

Связи между entity могут загружаться сразу или позже. Например у заказа может быть клиент:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;

LAZY означает: не загружать Customer сразу вместе с Order, а подгрузить при обращении к order.getCustomer(). EAGER означает: пытаться загрузить связанную сущность сразу. На первый взгляд EAGER проще, но в реальном приложении он часто приводит к лишним данным и тяжёлым запросам. Поэтому многие связи лучше держать lazy и явно выбирать, какие данные нужны для конкретного экрана или API.

LazyInitializationException возникает, когда код пытается обратиться к lazy-связи после окончания транзакции, когда Persistence Context уже закрыт. Hibernate хочет подгрузить данные, но рабочей области уже нет. Поэтому данные для ответа лучше загружать внутри service method и сразу преобразовывать в DTO.

N+1: почему возникает и как диагностировать

N+1 появляется, когда приложение сначала загружает список основных сущностей одним запросом, а затем для каждой сущности отдельно подгружает связанную сущность или коллекцию. Например один запрос получает 100 заказов, а потом 100 дополнительных запросов получают клиентов этих заказов. Итого получается 101 запрос.

В маленькой локальной базе это может быть незаметно. В production это быстро превращается в тормоза. Чтобы увидеть проблему, включают SQL logging и смотрят, сколько запросов реально выполняется. Решения зависят от задачи: fetch join, @EntityGraph, отдельный DTO query или ручной JPQL/native query.

Практический пример

Нужно создать endpoint оплаты заказа. Controller получает POST /orders/{id}/pay, service открывает транзакцию через @Transactional, repository загружает заказ, service меняет статус на PAID. Hibernate держит заказ как managed entity. В конце метода транзакция коммитится, Hibernate делает dirty checking и генерирует update.

Если в этом же ответе нужно вернуть имя клиента, service должен загрузить клиента внутри транзакции или сразу сделать DTO-запрос. Если просто вернуть entity наружу и потом где-то после транзакции обратиться к order.getCustomer().getName(), можно получить LazyInitializationException.

Итог этапа

Spring Data JPA — это не магия и не замена SQL-мышления. Entity описывает соответствие Java-класса таблице. JPA задаёт правила, Hibernate выполняет SQL по этим правилам, Spring Data JPA даёт удобные repository interfaces. @Transactional задаёт границу операции, внутри которой Hibernate может отслеживать managed entities и автоматически синхронизировать изменения с базой.

Чек-лист понимания

  • Могу объяснить, что @Entity, @Table, @Id и @Column делают в mapping.
  • Понимаю разницу между JPA, Hibernate и Spring Data JPA.
  • Понимаю, что SQL генерируется Hibernate на основе mapping и repository-вызовов.
  • Могу объяснить, зачем service method помечают @Transactional.
  • Понимаю, что managed entity отслеживается Hibernate внутри Persistence Context.
  • Могу объяснить причину LazyInitializationException и N+1 на простом примере.

Quiz

Проверьте, что вы усвоили

Авторизуйтесь чтоб пройти тесты