🗂️ Вопросы интервью по JPA

Спецификация, EntityManager, контекст персистентности и основные приёмы маппинга

Вопрос JPA 1
Что такое JPA и чем спецификация отличается от реализации?

Ответ: JPA (Jakarta Persistence API, ранее Java Persistence API) — это спецификация ORM для Java: набор интерфейсов, аннотаций и правил (EntityManager, @Entity, JPQL). Сама по себе JPA ничего не делает — нужна реализация (persistence provider).

Уровень Примеры
Спецификация JPA / Jakarta Persistence
Реализации Hibernate (самая популярная), EclipseLink, OpenJPA
Надстройки Spring Data JPA — репозитории поверх JPA
💡 Важно: начиная с версии 3.0 пакеты переименованы из javax.persistence в jakarta.persistence (Spring Boot 3+ использует jakarta). Код на чистом JPA можно переносить между реализациями, а специфичные для Hibernate аннотации (@Formula, @BatchSize) привязывают к нему.
Вопрос JPA 2
Каким требованиям должен отвечать класс-сущность (@Entity)?

Ответ:

  • Помечен аннотацией @Entity и имеет поле или свойство с @Id (первичный ключ)
  • Имеет конструктор без аргументов (public или protected)
  • Не является final — провайдер создаёт прокси-подклассы для ленивой загрузки; персистентные поля и методы тоже не должны быть final
  • Это верхнеуровневый класс или статический вложенный, не интерфейс и не enum
  • Поля с состоянием хранятся как private, доступ идёт через рефлексию или геттеры

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

    @Column(nullable = false, length = 100)
    private String name;

    protected User() { }                 // для JPA
    public User(String name) { this.name = name; }
}
                
💡 Важно: record нельзя использовать как сущность: он final, а его поля неизменяемы. Для DTO и проекций record подходит.
Вопрос JPA 3
Что такое EntityManager, EntityManagerFactory и persistence unit?

Ответ:

  • Persistence unit — именованная конфигурация: список сущностей, источник данных, свойства провайдера. Задаётся в persistence.xml или Spring Boot настраивает его автоматически
  • EntityManagerFactory — тяжёлый, потокобезопасный объект, создаётся один раз на persistence unit при старте приложения
  • EntityManager — лёгкий, не потокобезопасный объект для работы с сущностями (persist, find, merge, запросы). Один экземпляр соответствует одному контексту персистентности

EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-unit");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(new User("Ann"));
tx.commit();

em.close();
emf.close();
                
💡 В Spring: EntityManager внедряют через @PersistenceContext. Spring подставляет общий прокси, который сам привязывает реальный EntityManager к текущей транзакции.
Вопрос JPA 4
Что такое контекст персистентности (persistence context) и dirty checking?

Ответ: Контекст персистентности — это набор управляемых (managed) сущностей внутри одного EntityManager. Он работает как кэш первого уровня и гарантирует, что в рамках контекста одной строке БД соответствует один объект.

Что он делает:
  • Identity map — повторный find() с тем же id вернёт тот же объект без запроса в БД
  • Dirty checking — при flush провайдер сравнивает состояние управляемых сущностей со снимком, сделанным при загрузке, и сам формирует UPDATE
  • Отложенная запись — изменения накапливаются и отправляются в БД при flush или commit

em.getTransaction().begin();

User u = em.find(User.class, 1L);   // SELECT
User same = em.find(User.class, 1L); // запроса нет, берётся из контекста
System.out.println(u == same);       // true

u.setName("Bob");                    // явный update() не нужен

em.getTransaction().commit();        // UPDATE users SET name='Bob' ...
                
Вопрос JPA 5
В чём разница между persist, merge, remove, detach и refresh?
Метод Что делает
persist(e) Делает новую сущность управляемой, INSERT выполнится при flush. Для уже отсоединённой сущности — исключение
merge(e) Копирует состояние отсоединённого объекта в управляемый и возвращает управляемую копию; переданный объект остаётся отсоединённым
remove(e) Планирует удаление; сущность должна быть управляемой
detach(e) Убирает сущность из контекста; её изменения больше не отслеживаются
refresh(e) Перечитывает состояние из БД и затирает несохранённые изменения

User detached = ...;                 // получен в другом контексте
detached.setName("New");

User managed = em.merge(detached);   // работаем уже с managed
managed.setName("Newer");            // изменение будет сохранено

// detached.setName("X") — на БД не повлияет
                
💡 Частая ошибка: после merge продолжать менять исходный объект, а не возвращённый.
Вопрос JPA 6
Что такое flush и когда он происходит?

Ответ: Flush синхронизирует состояние контекста персистентности с базой: провайдер отправляет накопленные INSERT/UPDATE/DELETE. Flush не фиксирует транзакцию — данные становятся постоянными только после commit (при rollback отправленное откатывается).

Когда выполняется (режим FlushModeType.AUTO по умолчанию):
  • Перед commit транзакции
  • Перед выполнением JPQL-запроса, который может затронуть изменённые данные, чтобы запрос видел актуальное состояние
  • При явном вызове em.flush()

em.getTransaction().begin();
em.persist(new User("Ann"));
em.flush();                           // INSERT отправлен, но транзакция ещё открыта
em.getTransaction().rollback();       // запись откатывается
                
💡 Связано: find() по id сразу идёт в БД (если нет в контексте), а getReference() возвращает ленивый прокси без запроса; при обращении к отсутствующей записи будет EntityNotFoundException.
Вопрос JPA 7
Что такое JPQL и как использовать параметры, именованные запросы и пагинацию?

Ответ: JPQL — объектно-ориентированный язык запросов: работает с сущностями и их полями, а не с таблицами и колонками. Провайдер транслирует его в SQL.


// именованный параметр — защита от SQL-инъекций
List<User> users = em.createQuery(
        "SELECT u FROM User u WHERE u.name = :name ORDER BY u.id", User.class)
    .setParameter("name", "Ann")
    .setFirstResult(20)       // пагинация: пропустить 20
    .setMaxResults(10)        // вернуть не более 10
    .getResultList();

// именованный запрос: проверяется при старте приложения
@Entity
@NamedQuery(name = "User.byName",
            query = "SELECT u FROM User u WHERE u.name = :name")
public class User { ... }

em.createNamedQuery("User.byName", User.class)
  .setParameter("name", "Ann")
  .getSingleResult();
                
Другие способы запросов:
  • Criteria API — типобезопасное построение запросов в коде
  • Native query (createNativeQuery) — обычный SQL, когда нужны возможности конкретной СУБД
💡 Важно: никогда не склеивайте значения в строку JPQL — используйте параметры. getSingleResult() бросает NoResultException, если ничего не найдено, и NonUniqueResultException, если строк несколько.
Вопрос JPA 8
Что такое каскадные операции (CascadeType) и orphanRemoval?

Ответ: cascade распространяет операции EntityManager с родительской сущности на связанные.

Тип Что каскадируется
PERSIST persist() родителя сохраняет и детей
MERGE, REFRESH, DETACH Соответствующие операции
REMOVE При удалении родителя удаляются дети
ALL Все перечисленные

orphanRemoval = true дополнительно удаляет ребёнка, когда его убрали из коллекции родителя (он стал «сиротой»), даже если родитель остался.


@Entity
class Order {
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
}

order.getItems().remove(item);   // item будет удалён из БД при flush
                
💡 Важно: CascadeType.ALL с @ManyToOne и @ManyToMany опасен: удаление одной записи может стереть общие данные. Каскад имеет смысл для отношений «часть — целое».
Вопрос JPA 9
Что такое владеющая сторона связи и атрибут mappedBy?

Ответ: В двунаправленной связи только одна сторона управляет внешним ключом в БД — владеющая (owning side). Вторая — обратная (inverse) — помечается mappedBy и указывает на поле владельца.

  • Владеющая сторона — та, где находится внешний ключ; в @OneToMany/@ManyToOne это всегда @ManyToOne
  • Изменения, сделанные только на обратной стороне, в БД не попадут
  • Без mappedBy односторонний @OneToMany создаёт лишнюю join-таблицу

@Entity
class Order {
    @OneToMany(mappedBy = "order")           // обратная сторона
    private List<OrderItem> items = new ArrayList<>();

    public void addItem(OrderItem item) {    // синхронизируем обе стороны
        items.add(item);
        item.setOrder(this);
    }
}

@Entity
class OrderItem {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")           // владеющая сторона, здесь FK
    private Order order;
}
                
💡 Совет: всегда делайте вспомогательные методы addX/removeX, которые обновляют обе стороны, чтобы объектная модель и БД не расходились.
Вопрос JPA 10
Какие стратегии наследования поддерживает JPA?

Ответ: Задаются аннотацией @Inheritance:

Стратегия Как хранится Плюсы / минусы
SINGLE_TABLE (по умолчанию) Вся иерархия в одной таблице + колонка-дискриминатор Быстро, без JOIN; но поля подклассов допускают NULL
JOINED Таблица на каждый класс, связаны по PK Нормализованная схема; но нужны JOIN
TABLE_PER_CLASS Отдельная полная таблица на каждый конкретный класс Полиморфные запросы через UNION медленные; поддержка провайдером необязательна

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
abstract class Payment { @Id Long id; }

@Entity @DiscriminatorValue("CARD")
class CardPayment extends Payment { String cardNumber; }
                
💡 Отличие: @MappedSuperclass — не сущность, а общий родитель с полями (например id, createdAt), который не имеет своей таблицы и не участвует в полиморфных запросах.
Вопрос JPA 11
Что такое @Embeddable, @Embedded и @ElementCollection?

Ответ:

  • @Embeddable — класс-значение без собственного идентификатора (адрес, деньги). Его поля хранятся в таблице владельца
  • @Embedded — поле сущности, которое использует такой класс
  • @AttributeOverride — переименовывает колонки, если один тип встроен несколько раз
  • @ElementCollection — коллекция простых значений или embeddable-объектов; хранится в отдельной таблице, у элементов нет своего id

@Embeddable
class Address {
    private String city;
    private String street;
}

@Entity
class Customer {
    @Id Long id;

    @Embedded
    private Address home;

    @Embedded
    @AttributeOverride(name = "city", column = @Column(name = "work_city"))
    private Address work;

    @ElementCollection
    private Set<String> phones = new HashSet<>();
}
                
Вопрос JPA 12
Что такое LazyInitializationException и как её исправить?

Ответ: Исключение возникает, когда обращаются к ленивой ассоциации или коллекции после закрытия сессии/EntityManager (объект уже отсоединён), и провайдер не может дозагрузить данные.


@Transactional
public Order load(Long id) {
    return em.find(Order.class, id);      // items ещё не загружены
}

Order o = service.load(1L);
o.getItems().size();   // LazyInitializationException: no Session
                
Решения:
  • JOIN FETCH в запросе: SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id
  • Entity graph (@EntityGraph или EntityGraph API) — загрузка нужных связей для конкретного запроса
  • DTO-проекция — выбирать только нужные поля, не возвращать сущность в контроллер
  • Обращаться к коллекции внутри транзакции в сервисе
💡 Чего делать не стоит: менять все связи на EAGER — это ведёт к лишним запросам и N+1. Open Session in View (spring.jpa.open-in-view) скрывает ошибку, но держит соединение с БД открытым на всё время обработки запроса.
Вопрос JPA 13
Как работают оптимистичные и пессимистичные блокировки в JPA?
Оптимистичная (по умолчанию, через @Version):
  • В сущность добавляется поле версии (int, long, Timestamp)
  • Провайдер делает UPDATE ... WHERE id = ? AND version = ? и увеличивает версию
  • Если запись успел изменить другой — обновлено 0 строк, выбрасывается OptimisticLockException
  • Блокировок в БД нет, подходит для редких конфликтов

@Entity
class Account {
    @Id Long id;
    @Version
    private Long version;
    private BigDecimal balance;
}
                
Пессимистичная:
  • Блокирует строку в БД на время транзакции: SELECT ... FOR UPDATE
  • Режимы: PESSIMISTIC_READ, PESSIMISTIC_WRITE, PESSIMISTIC_FORCE_INCREMENT
  • Подходит для частых конфликтов, но риск взаимоблокировок и ожиданий

Account acc = em.find(Account.class, 1L, LockModeType.PESSIMISTIC_WRITE);
acc.setBalance(acc.getBalance().subtract(amount));
                
Вопрос JPA 14
Как правильно реализовать equals() и hashCode() для сущности?

Ответ: Основная проблема — идентификатор, сгенерированный БД, равен null до persist. Если hashCode зависит от id, он меняется после сохранения, и сущность «теряется» в HashSet и HashMap.

Варианты:
  • Бизнес-ключ — уникальное неизменяемое поле (email, ISBN), по нему equals/hashCode
  • Поле-идентификатор: equals сравнивает id (при не-null), а hashCode возвращает константу — безопасно, но все элементы окажутся в одной корзине
  • UUID, присвоенный в конструкторе, — стабилен с момента создания

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User other)) return false;    // instanceof, а не getClass(): учитывает прокси
    return id != null && id.equals(other.getId());
}

@Override
public int hashCode() {
    return getClass().hashCode();   // константа, не зависит от id
}
                
💡 Важно: не используйте Lombok @Data/@EqualsAndHashCode на сущностях «как есть»: он включает все поля, в том числе ленивые связи, и может вызвать дополнительные запросы и бесконечную рекурсию.