1. Введение
В этом руководстве мы обсудим различия между несколькими методами интерфейса Session :
save
, persist
, update
, merge
и saveOrUpdate
.
Это не введение в Hibernate, и мы уже должны знать основы настройки, объектно-реляционного сопоставления и работы с экземплярами сущностей. Для получения вводной статьи о Hibernate посетите наш учебник по Hibernate 4 с Spring .
2. Сессия как реализация контекста персистентности
Интерфейс Session
имеет несколько методов, которые в конечном итоге приводят к сохранению данных в базу данных: persist
, save
, update
, merge
и saveOrUpdate
. Чтобы понять разницу между этими методами, мы должны сначала обсудить назначение Session
как контекста персистентности и разницу между состояниями экземпляров сущностей по отношению к Session
.
Мы также должны понимать историю развития Hibernate, которая привела к частичному дублированию методов API.
2.1. Управление экземплярами сущностей
Помимо собственно объектно-реляционного отображения, одной из проблем, которые решает Hibernate, является проблема управления сущностями во время выполнения. Понятие «контекст персистентности» — это решение Hibernate этой проблемы. Мы можем думать о контексте персистентности как о контейнере или кеше первого уровня для всех объектов, которые мы загрузили или сохранили в базе данных во время сеанса.
Сеанс представляет собой логическую транзакцию, границы которой определяются бизнес-логикой приложения. Когда мы работаем с базой данных через контекст постоянства, и все экземпляры наших сущностей привязаны к этому контексту, у нас всегда должен быть один экземпляр сущности для каждой записи базы данных, с которой мы взаимодействуем во время сеанса.
В Hibernate контекст сохранения представлен экземпляром org.hibernate.Session
. Для JPA это javax.persistence.EntityManager
. Когда мы используем Hibernate в качестве провайдера JPA и работаем через интерфейс EntityManager
, реализация этого интерфейса в основном обертывает базовый объект Session .
Однако Hibernate Session
предоставляет более богатый интерфейс с большими возможностями, поэтому иногда полезно работать непосредственно с Session .
2.2. Состояния экземпляров сущностей
Любой экземпляр объекта в нашем приложении отображается в одном из трех основных состояний по отношению к контексту сохраняемости сеанса :
transient
— этот экземпляр никогда не был привязан ксеансу.
У этого экземпляра нет соответствующих строк в базе данных; обычно это просто новый объект, который мы создали для сохранения в базе данных.постоянный
— этот экземпляр связан с уникальным объектомсеанса .
После сбросасеанса
в базу данных этот объект гарантированно будет иметь соответствующую непротиворечивую запись в базе данных.detached
— этот экземпляр когда-то был присоединен ксеансу
(впостоянном
состоянии), но теперь это не так. Экземпляр входит в это состояние, если мы исключим его из контекста, очистим или закроем сессию или поместим экземпляр в процесс сериализации/десериализации.
Вот упрощенная диаграмма состояний с комментариями к методам сеанса
, которые вызывают переходы между состояниями:
Когда экземпляр сущности находится в постоянном
состоянии, все изменения, которые мы вносим в сопоставленные поля этого экземпляра, будут применены к соответствующим записям и полям базы данных при сбросе Session
. Постоянный экземпляр
находится «в сети», тогда как отсоединенный
экземпляр находится «в автономном режиме» и не отслеживается на наличие изменений.
Это означает, что когда мы изменяем поля постоянного
объекта, нам не нужно вызывать save
, update
или любой из этих методов, чтобы получить эти изменения в базе данных. Все, что нам нужно сделать, это зафиксировать транзакцию, очистить сеанс или закрыть сеанс.
2.3. Соответствие спецификации JPA
Hibernate был самой успешной реализацией Java ORM. Таким образом, Hibernate API сильно повлиял на спецификации Java Persistence API (JPA). К сожалению, было также много отличий, как серьезных, так и более тонких.
Чтобы действовать как реализация стандарта JPA, Hibernate API пришлось пересмотреть. Чтобы соответствовать интерфейсу EntityManager , в интерфейс
Session
было добавлено несколько методов . Эти методы служат той же цели, что и исходные методы, но соответствуют спецификации и поэтому имеют некоторые отличия.
3. Различия между операциями
С самого начала важно понимать, что все методы ( persist
, save
, update
, merge
, saveOrUpdate
) не сразу приводят к соответствующим операторам SQL UPDATE
или INSERT
. Фактическое сохранение данных в базе данных происходит при фиксации транзакции или сбросе Session
.
Упомянутые методы в основном управляют состоянием экземпляров сущностей, переводя их между различными состояниями в течение жизненного цикла.
В качестве примера мы будем использовать простой объект с аннотациями, Person
:
@Entity
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
// ... getters and setters
}
3.1. Сопротивляться
Метод persist
предназначен для добавления нового экземпляра сущности в контекст сохраняемости, т. е. для перевода экземпляра из переходного
состояния в постоянное
.
Обычно мы вызываем его, когда хотим добавить запись в базу данных (сохранить экземпляр сущности):
Person person = new Person();
person.setName("John");
session.persist(person);
Что произойдет после того, как мы вызовем метод persist
? Объект person
перешел из переходного
состояния в постоянное
. Объект сейчас находится в контексте постоянства, но еще не сохранен в базе данных. Генерация операторов INSERT
будет происходить только после фиксации транзакции, сброса или закрытия сеанса.
Обратите внимание, что метод persist
имеет возвращаемый тип void .
Он работает с переданным объектом «на месте», изменяя его состояние. Переменная person
ссылается на реальный сохраняемый объект.
Этот метод является более поздним дополнением к интерфейсу Session .
Главной отличительной чертой этого метода является то, что он соответствует спецификации JSR-220 (постоянство EJB). Мы строго определяем семантику этого метода в спецификации, которая в основном утверждает, что временный
экземпляр становится постоянным
(и операция каскадируется на все его отношения с cascade=PERSIST
или cascade=ALL
):
- если экземпляр уже является
постоянным
, то этот вызов не имеет никакого эффекта для этого конкретного экземпляра (но он по-прежнему каскадируется на его отношения сcascade=PERSIST
илиcascade=ALL
). - если экземпляр
detached
, мы получим исключение либо при вызове этого метода, либо при фиксации или сбросе сеанса.
Обратите внимание, что здесь нет ничего, что касалось бы идентификатора экземпляра. В спецификации не указано, что идентификатор будет сгенерирован сразу, независимо от стратегии генерации идентификатора. Спецификация метода persist
позволяет реализации создавать операторы для генерации идентификатора при фиксации или сбросе. Идентификатор не обязательно будет ненулевым после того, как мы вызовем этот метод, поэтому мы не должны полагаться на него.
Мы можем вызвать этот метод для уже сохраняющегося
экземпляра, и ничего не происходит. Но если мы попытаемся сохранить отсоединенный
экземпляр, реализация выдаст исключение. В следующем примере мы сохраним
сущность, исключим
ее из контекста, чтобы она стала detached
, а затем снова попытаемся сохранить
. Второй вызов session.persist()
вызывает исключение, поэтому следующий код работать не будет:
Person person = new Person();
person.setName("John");
session.persist(person);
session.evict(person);
session.persist(person); // PersistenceException!
3.2. Сохранять
Метод сохранения
— это «оригинальный» метод Hibernate, который не соответствует спецификации JPA.
Его цель в основном такая же, как и persist
, но у него другие детали реализации. В документации к этому методу строго указано, что он сохраняет экземпляр, «сначала присваивая сгенерированный идентификатор». Метод вернет Serializable
значение этого идентификатора:
Person person = new Person();
person.setName("John");
Long id = (Long) session.save(person);
Эффект сохранения уже сохраненного экземпляра такой же, как и при использовании persist
. Разница возникает, когда мы пытаемся сохранить отдельный
экземпляр:
Person person = new Person();
person.setName("John");
Long id1 = (Long) session.save(person);
session.evict(person);
Long id2 = (Long) session.save(person);
Переменная id2
будет отличаться от id1
. Вызов сохранения для отсоединенного
экземпляра создает новый постоянный
экземпляр и назначает ему новый идентификатор, что приводит к дублированию записи в базе данных при фиксации или сбросе.
3.3. Объединить
Основное назначение метода слияния
— обновить экземпляр постоянной
сущности новыми значениями полей из экземпляра отсоединенной
сущности.
Например, предположим, что у нас есть интерфейс RESTful с методом для получения JSON-сериализованного объекта по его идентификатору для вызывающей стороны и методом, который получает обновленную версию этого объекта от вызывающей стороны. Сущность, прошедшая такую сериализацию/десериализацию, появится в отсоединенном
состоянии.
После десериализации этого экземпляра объекта нам нужно получить экземпляр постоянного
объекта из контекста сохранения и обновить его поля новыми значениями из этого отсоединенного
экземпляра. Таким образом, метод слияния
делает именно это:
- находит экземпляр сущности по идентификатору, взятому из переданного объекта (либо извлекается существующий экземпляр сущности из контекста персистентности, либо загружается новый экземпляр из базы данных)
- копирует поля из переданного объекта в этот экземпляр
- возвращает недавно обновленный экземпляр
В следующем примере мы исключаем
(отсоединяем) сохраненную сущность из контекста, меняем поле имени
, а затем объединяем
отсоединенную
сущность :
Person person = new Person();
person.setName("John");
session.save(person);
session.evict(person);
person.setName("Mary");
Person mergedPerson = (Person) session.merge(person);
Обратите внимание, что метод слияния
возвращает объект. Это объект mergedPerson
, который мы загрузили в контекст постоянства и обновили, а не объект person
, который мы передали в качестве аргумента. Это два разных объекта, и обычно нам нужно отбросить объект человека .
Как и в случае с методом persist
, метод слияния
определяется JSR-220, чтобы иметь определенную семантику, на которую мы можем положиться:
- если сущность
отсоединена
, она копируется в существующуюпостоянную
сущность. - если объект
временный
, он копируется во вновь созданныйпостоянный
объект. - эта операция каскадируется для всех отношений с отображением
cascade=MERGE
илиcascade=ALL
. - если объект является
постоянным
, то этот вызов метода не влияет на него (но каскадирование все равно происходит).
3.4. Обновлять
Как и в случае с persist
и save
, метод update
является «оригинальным» методом Hibernate. Его семантика отличается в нескольких ключевых моментах:
- он действует на переданный объект (тип возвращаемого значения —
void
). Методобновления
переводит переданный объект изотсоединенного
впостоянное
состояние. - этот метод выдает исключение, если мы передаем ему
временную
сущность.
В следующем примере мы сохраняем
объект, исключаем
(отключаем) его из контекста, а затем меняем его имя
и вызываем update
. Обратите внимание, что мы не помещаем результат операции обновления
в отдельную переменную, потому что обновление
происходит в самом объекте человека .
По сути, мы повторно присоединяем существующий экземпляр объекта к контексту персистентности, что не позволяет нам делать спецификация JPA:
Person person = new Person();
person.setName("John");
session.save(person);
session.evict(person);
person.setName("Mary");
session.update(person);
Попытка вызвать обновление
для временного
экземпляра приведет к исключению. Следующее не будет работать:
Person person = new Person();
person.setName("John");
session.update(person); // PersistenceException!
3.5. Сохранить или обновить
Этот метод присутствует только в Hibernate API и не имеет своего стандартизированного аналога. Подобно update
, мы также можем использовать его для повторного подключения экземпляров.
Фактически, внутренний класс DefaultUpdateEventListener
, который обрабатывает метод обновления
, является подклассом DefaultSaveOrUpdateListener
, просто переопределяющим некоторые функции. Основное отличие метода saveOrUpdate
заключается в том, что он не генерирует исключение при применении к временному
экземпляру, вместо этого он делает этот временный
экземпляр постоянным
. Следующий код сохранит только что созданный экземпляр Person
:
Person person = new Person();
person.setName("John");
session.saveOrUpdate(person);
Мы можем думать об этом методе как об универсальном инструменте для того, чтобы сделать объект постоянным
независимо от его состояния, будь то переходный
или отсоединенный
.
4. Что использовать?
Если у нас нет особых требований, мы должны придерживаться методов persist
и merge
, потому что они стандартизированы и будут соответствовать спецификации JPA.
Они также переносимы на случай, если мы решим переключиться на другого поставщика сохраняемости; однако иногда они могут показаться не такими полезными, как «оригинальные» методы Hibernate, save
, update
и saveOrUpdate
.
5. Вывод
В этой статье мы обсудили назначение различных методов Hibernate Session в отношении управления постоянными сущностями во время выполнения. Мы узнали, как эти методы передают экземпляры сущностей на протяжении их жизненного цикла и почему некоторые из этих методов имеют дублирующую функциональность.
Исходный код статьи доступен на GitHub .