Спецификация, EntityManager, контекст персистентности и основные приёмы маппинга
Ответ: 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 |
javax.persistence в jakarta.persistence (Spring Boot 3+ использует jakarta). Код на чистом JPA можно переносить между реализациями, а специфичные для Hibernate аннотации (@Formula, @BatchSize) привязывают к нему.
Ответ:
@Entity и имеет поле или свойство с @Id (первичный ключ)public или protected)final — провайдер создаёт прокси-подклассы для ленивой загрузки; персистентные поля и методы тоже не должны быть finalprivate, доступ идёт через рефлексию или геттеры
@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 подходит.
Ответ:
persistence.xml или Spring Boot настраивает его автоматически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();
EntityManager внедряют через @PersistenceContext. Spring подставляет общий прокси, который сам привязывает реальный EntityManager к текущей транзакции.
Ответ: Контекст персистентности — это набор управляемых (managed) сущностей внутри одного EntityManager. Он работает как кэш первого уровня и гарантирует, что в рамках контекста одной строке БД соответствует один объект.
find() с тем же id вернёт тот же объект без запроса в БДUPDATE
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' ...
| Метод | Что делает |
|---|---|
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 продолжать менять исходный объект, а не возвращённый.
Ответ: Flush синхронизирует состояние контекста персистентности с базой: провайдер отправляет накопленные INSERT/UPDATE/DELETE. Flush не фиксирует транзакцию — данные становятся постоянными только после commit (при rollback отправленное откатывается).
FlushModeType.AUTO по умолчанию):em.flush()
em.getTransaction().begin();
em.persist(new User("Ann"));
em.flush(); // INSERT отправлен, но транзакция ещё открыта
em.getTransaction().rollback(); // запись откатывается
find() по id сразу идёт в БД (если нет в контексте), а getReference() возвращает ленивый прокси без запроса; при обращении к отсутствующей записи будет EntityNotFoundException.
Ответ: 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();
createNativeQuery) — обычный SQL, когда нужны возможности конкретной СУБДgetSingleResult() бросает NoResultException, если ничего не найдено, и NonUniqueResultException, если строк несколько.
Ответ: 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 опасен: удаление одной записи может стереть общие данные. Каскад имеет смысл для отношений «часть — целое».
Ответ: В двунаправленной связи только одна сторона управляет внешним ключом в БД — владеющая (owning side). Вторая — обратная (inverse) — помечается mappedBy и указывает на поле владельца.
@OneToMany/@ManyToOne это всегда @ManyToOnemappedBy односторонний @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, которые обновляют обе стороны, чтобы объектная модель и БД не расходились.
Ответ: Задаются аннотацией @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), который не имеет своей таблицы и не участвует в полиморфных запросах.
Ответ:
@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<>();
}
Ответ: Исключение возникает, когда обращаются к ленивой ассоциации или коллекции после закрытия сессии/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
SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id@EntityGraph или EntityGraph API) — загрузка нужных связей для конкретного запросаEAGER — это ведёт к лишним запросам и N+1. Open Session in View (spring.jpa.open-in-view) скрывает ошибку, но держит соединение с БД открытым на всё время обработки запроса.
int, long, Timestamp)UPDATE ... WHERE id = ? AND version = ? и увеличивает версиюOptimisticLockException
@Entity
class Account {
@Id Long id;
@Version
private Long version;
private BigDecimal balance;
}
SELECT ... FOR UPDATEPESSIMISTIC_READ, PESSIMISTIC_WRITE, PESSIMISTIC_FORCE_INCREMENT
Account acc = em.find(Account.class, 1L, LockModeType.PESSIMISTIC_WRITE);
acc.setBalance(acc.getBalance().subtract(amount));
Ответ: Основная проблема — идентификатор, сгенерированный БД, равен null до persist. Если hashCode зависит от id, он меняется после сохранения, и сущность «теряется» в HashSet и HashMap.
equals/hashCodeequals сравнивает id (при не-null), а hashCode возвращает константу — безопасно, но все элементы окажутся в одной корзине
@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
}
@Data/@EqualsAndHashCode на сущностях «как есть»: он включает все поля, в том числе ленивые связи, и может вызвать дополнительные запросы и бесконечную рекурсию.