Как правильно повторно прикрепить отдельные объекты в Hibernate?
У меня есть ситуация, в которой мне нужно повторно присоединить отдельные объекты к сеансу гибернации, хотя в сеансе МОЖЕТ уже существовать объект с той же идентичностью, что приведет к ошибкам.
Прямо сейчас я могу сделать одну из двух вещей.
getHibernateTemplate().update( obj )
Это работает тогда и только тогда, когда объект не существует в сеансе гибернации. Выдаются исключения, указывающие, что объект с данным идентификатором уже существует в сеансе, когда он мне понадобится позже.getHibernateTemplate().merge( obj )
Это работает, если и только если объект существует в сеансе гибернации. Исключения генерируются, когда мне нужно, чтобы объект был в сеансе позже, если я использую это.
Учитывая эти два сценария, как я могу в общем присоединять сеансы к объектам? Я не хочу использовать исключения для управления потоком решения этой проблемы, так как должно быть более элегантное решение...
15 ответов
Таким образом, кажется, что в JPA нет способа присоединить устаревшую отдельную сущность.
merge()
перенесет устаревшее состояние в БД и перезапишет любые промежуточные обновления.
refresh()
не может быть вызван на отдельном объекте.
lock()
не может быть вызван для отсоединенной сущности, и даже если бы он мог, и он снова прикрепил сущность, вызов 'lock' с аргументом 'LockMode.NONE', подразумевающий, что вы блокируете, но не блокируете, является наиболее противоречивым элементом разработки API Я когда-либо видел.
Итак, вы застряли. Там есть detach()
метод, но нет attach()
или же reattach()
, Очевидный шаг в жизненном цикле объекта вам недоступен.
Судя по количеству подобных вопросов о JPA, кажется, что даже если JPA действительно утверждает, что имеет согласованную модель, она, безусловно, не соответствует ментальной модели большинства программистов, которые были прокляты тратить много часов, пытаясь понять, как получить JPA, чтобы делать самые простые вещи, и в конечном итоге с кодом управления кэшем во всех своих приложениях.
Кажется, единственный способ сделать это - сбросить устаревшую отдельную сущность и выполнить запрос на поиск с тем же идентификатором, который попадет на L2 или в базу данных.
Mik
Поскольку это очень частый вопрос, я написал эту статью, на которой основан этот ответ.
Состояния сущности
JPA определяет следующие состояния сущностей:
Новый (временный)
Недавно созданный объект, который никогда не был связан с Hibernate. Session
(он же Persistence Context
) и не сопоставлен ни с одной строкой таблицы базы данных, считается находящейся в состоянии New (Transient).
Чтобы стать устойчивым, нам нужно либо явно вызвать EntityManager#persist
метод или использовать механизм переходной персистентности.
Постоянный (управляемый)
Постоянный объект был связан со строкой таблицы базы данных, и он управляется текущим текущим контекстом сохранения. Любые изменения, внесенные в такой объект, будут обнаружены и распространены в базу данных (во время сброса сеанса).
С Hibernate нам больше не нужно выполнять инструкции INSERT/UPDATE/DELETE. Hibernate использует стиль работы с отложенной записью транзакций, и изменения синхронизируются в самый последний ответственный момент, в течение текущегоSession
время промывки.
Отдельно
После закрытия текущего работающего контекста постоянства все ранее управляемые объекты отключаются. Последовательные изменения больше не будут отслеживаться, и автоматическая синхронизация базы данных не произойдет.
Переходы между состояниями объекта
Вы можете изменить состояние объекта, используя различные методы, определенные EntityManager
интерфейс.
Чтобы лучше понять переходы состояний сущностей JPA, рассмотрим следующую диаграмму:
При использовании JPA для повторного связывания отсоединенного объекта с активным EntityManager
, вы можете использовать операцию слияния.
При использовании собственного Hibernate API, кроме merge
, вы можете повторно подключить отсоединенный объект к активному сеансу Hibernate, используя методы обновления, как показано на следующей диаграмме:
Слияние отдельного объекта
Слияние будет копировать состояние отсоединенного объекта (источник) в экземпляр управляемого объекта (место назначения).
Считайте, что мы настаиваем на следующем Book
сущность, и теперь сущность отсоединяется как EntityManager
который использовался для сохранения закрытой сущности:
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
entityManager.persist(book);
return book;
});
Пока объект находится в отсоединенном состоянии, мы модифицируем его следующим образом:
_book.setTitle(
"High-Performance Java Persistence, 2nd edition"
);
Теперь мы хотим распространить изменения в базу данных, поэтому мы можем вызвать merge
метод:
doInJPA(entityManager -> {
Book book = entityManager.merge(_book);
LOGGER.info("Merging the Book entity");
assertFalse(book == _book);
});
И Hibernate будет выполнять следующие операторы SQL:
SELECT
b.id,
b.author AS author2_0_,
b.isbn AS isbn3_0_,
b.title AS title4_0_
FROM
book b
WHERE
b.id = 1
-- Merging the Book entity
UPDATE
book
SET
author = 'Vlad Mihalcea',
isbn = '978-9730228236',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
Если у объединяемого объекта нет эквивалента в текущем EntityManager
, из базы данных будет получен свежий снимок объекта.
Как только существует управляемая сущность, JPA копирует состояние отсоединенной сущности на ту, которая в настоящее время управляется, и во время контекста постоянстваflush
, ОБНОВЛЕНИЕ будет сгенерировано, если механизм грязной проверки обнаружит, что управляемый объект изменился.
Итак, при использовании
merge
, экземпляр отсоединенного объекта будет оставаться отсоединенным даже после операции слияния.
Повторное присоединение отдельного объекта
Hibernate, но не JPA, поддерживает повторное подключение через update
метод.
Спящий режим Session
может связать только один объект сущности с данной строкой базы данных. Это связано с тем, что контекст постоянства действует как кэш в памяти (кэш первого уровня), и только одно значение (сущность) связано с данным ключом (типом сущности и идентификатором базы данных).
Сущность может быть повторно присоединена, только если нет другого объекта JVM (соответствующего той же строке базы данных), уже связанного с текущим Hibernate. Session
.
Учитывая, что мы сохранили Book
сущность и что мы изменили его, когда Book
объект был в обособленном состоянии:
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
entityManager.persist(book);
return book;
});
_book.setTitle(
"High-Performance Java Persistence, 2nd edition"
);
Мы можем повторно прикрепить отсоединенный объект следующим образом:
doInJPA(entityManager -> {
Session session = entityManager.unwrap(Session.class);
session.update(_book);
LOGGER.info("Updating the Book entity");
});
И Hibernate выполнит следующий оператор SQL:
-- Updating the Book entity
UPDATE
book
SET
author = 'Vlad Mihalcea',
isbn = '978-9730228236',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
В
update
метод требует от васunwrap
тоEntityManager
в спящий режимSession
.
В отличие от merge
, предоставленный отсоединенный объект будет повторно ассоциирован с текущим контекстом постоянства, и UPDATE планируется во время сброса независимо от того, изменился объект или нет.
Чтобы предотвратить это, вы можете использовать @SelectBeforeUpdate
Аннотация Hibernate, которая запускает инструкцию SELECT, которая извлекает загруженное состояние, которое затем используется механизмом грязной проверки.
@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {
//Code omitted for brevity
}
Остерегайтесь исключения NonUniqueObjectException
Одна проблема, которая может возникнуть с update
это если контекст постоянства уже содержит ссылку на сущность с тем же идентификатором и тем же типом, что и в следующем примере:
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
Session session = entityManager.unwrap(Session.class);
session.saveOrUpdate(book);
return book;
});
_book.setTitle(
"High-Performance Java Persistence, 2nd edition"
);
try {
doInJPA(entityManager -> {
Book book = entityManager.find(
Book.class,
_book.getId()
);
Session session = entityManager.unwrap(Session.class);
session.saveOrUpdate(_book);
});
} catch (NonUniqueObjectException e) {
LOGGER.error(
"The Persistence Context cannot hold " +
"two representations of the same entity",
e
);
}
Теперь при выполнении приведенного выше тестового примера Hibernate выбрасывает NonUniqueObjectException
потому что второй EntityManager
уже содержит Book
сущность с тем же идентификатором, что и тот, которому мы передаем update
, и контекст сохранения не может содержать два представления одной и той же сущности.
org.hibernate.NonUniqueObjectException:
A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]
at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)
at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)
at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)
Вывод
В merge
Если вы используете оптимистическую блокировку, предпочтительнее использовать этот метод, поскольку он позволяет предотвратить потерю обновлений. Подробнее об этой теме читайте в этой статье.
В update
подходит для пакетных обновлений, поскольку может предотвратить дополнительный оператор SELECT, сгенерированный merge
операции, что сокращает время выполнения пакетного обновления.
Все эти ответы пропускают важное различие. update () используется для (повторного) присоединения вашего графа объектов к сеансу. Объекты, которые вы передаете, - это те, которые сделаны управляемыми.
merge () на самом деле не является (пере) приложением API. Заметьте, что merge () имеет возвращаемое значение? Это потому, что он возвращает вам управляемый граф, который может не совпадать с графиком, который вы передали. merge () является API-интерфейсом JPA, и его поведение регулируется спецификацией JPA. Если объект, который вы передаете в merge (), уже управляем (уже связан с Session), то с этим графом работает Hibernate; переданный объект - это тот же объект, который возвращается из функции merge (). Однако если объект, который вы передаете в merge (), отсоединен, Hibernate создает новый управляемый граф объектов и копирует состояние из вашего отдельного графа в новый управляемый граф. Опять же, все это продиктовано и регулируется спецификацией JPA.
С точки зрения общей стратегии "убедитесь, что этот объект управляется, или сделайте его управляемым", это отчасти зависит от того, хотите ли вы учесть еще не вставленные данные. Предполагая, что вы делаете, используйте что-то вроде
if ( session.contains( myEntity ) ) {
// nothing to do... myEntity is already associated with the session
}
else {
session.saveOrUpdate( myEntity );
}
Обратите внимание, что я использовал saveOrUpdate (), а не update (). Если вы не хотите обрабатывать еще не вставленные данные, используйте update () вместо этого...
Недипломатичный ответ: Вы, вероятно, ищете расширенный контекст постоянства. Это одна из основных причин Seam Framework... Если вы, в частности, пытаетесь использовать Hibernate в Spring, ознакомьтесь с этой частью документов Seam.
Дипломатический ответ: это описано в документах Hibernate. Если вам нужно больше разъяснений, взгляните на Раздел 9.3.2 Java Persistence с Hibernate под названием "Работа с отделенными объектами". Я настоятельно рекомендую вам приобрести эту книгу, если вы делаете что-то большее, чем CRUD с Hibernate.
Если вы уверены, что ваша сущность не была изменена (или если вы согласны с тем, что любая модификация будет потеряна), вы можете присоединить ее к сеансу с блокировкой.
session.lock(entity, LockMode.NONE);
Он ничего не заблокирует, но получит объект из кэша сеанса или (если его там не найден) прочитает его из БД.
Очень полезно предотвращать LazyInitException, когда вы перемещаетесь по отношениям со "старых" (например, из HttpSession) сущностей. Сначала вы заново "присоединяете" сущность.
Использование get также может работать, за исключением случаев, когда вы получите отображение наследования (которое уже вызовет исключение в getId()).
entity = session.get(entity.getClass(), entity.getId());
Я вернулся в JavaDoc для org.hibernate.Session
и нашел следующее:
Временные случаи могут быть сделаны постоянными, вызывая
save()
,persist()
или жеsaveOrUpdate()
, Постоянные экземпляры могут быть сделаны переходными, вызываяdelete()
, Любой экземпляр, возвращенныйget()
или жеload()
метод является постоянным. Отдельные экземпляры могут быть сделаны постоянными путем вызоваupdate()
,saveOrUpdate()
,lock()
или жеreplicate()
, Состояние временного или отдельного экземпляра также можно сделать постоянным как новый постоянный экземпляр, вызвавmerge()
,
таким образом update()
, saveOrUpdate()
, lock()
, replicate()
а также merge()
варианты кандидата.
update()
: Выдает исключение, если существует постоянный экземпляр с тем же идентификатором.
saveOrUpdate()
: Либо сохранить, либо обновить
lock()
: Устарел
replicate()
: Сохранить состояние данного отдельного экземпляра, повторно используя текущее значение идентификатора.
merge()
Возвращает постоянный объект с тем же идентификатором. Данный экземпляр не становится связанным с сеансом.
Следовательно, lock()
не следует использовать сразу и исходя из функциональных требований, можно выбрать один или несколько из них.
Я сделал это таким образом в C# с NHibernate, но он должен работать так же, как в Java:
public virtual void Attach()
{
if (!HibernateSessionManager.Instance.GetSession().Contains(this))
{
ISession session = HibernateSessionManager.Instance.GetSession();
using (ITransaction t = session.BeginTransaction())
{
session.Lock(this, NHibernate.LockMode.None);
t.Commit();
}
}
}
Первый замок был вызван для каждого объекта, потому что Contains всегда был ложным. Проблема в том, что NHibernate сравнивает объекты по идентификатору и типу базы данных. Содержит использует equals
метод, который сравнивает по ссылке, если он не перезаписан. С этим equals
Метод работает без каких-либо исключений:
public override bool Equals(object obj)
{
if (this == obj) {
return true;
}
if (GetType() != obj.GetType()) {
return false;
}
if (Id != ((BaseObject)obj).Id)
{
return false;
}
return true;
}
Session.contains(Object obj)
проверяет ссылку и не обнаруживает другой экземпляр, который представляет ту же строку и уже присоединен к ней.
Вот мое общее решение для сущностей со свойством идентификатора.
public static void update(final Session session, final Object entity)
{
// if the given instance is in session, nothing to do
if (session.contains(entity))
return;
// check if there is already a different attached instance representing the same row
final ClassMetadata classMetadata = session.getSessionFactory().getClassMetadata(entity.getClass());
final Serializable identifier = classMetadata.getIdentifier(entity, (SessionImplementor) session);
final Object sessionEntity = session.load(entity.getClass(), identifier);
// override changes, last call to update wins
if (sessionEntity != null)
session.evict(sessionEntity);
session.update(entity);
}
Это один из немногих аспектов.Net EntityFramework, которые мне нравятся, - различные параметры присоединения, касающиеся измененных объектов и их свойств.
Я предложил решение "обновить" объект из постоянного хранилища, который будет учитывать другие объекты, которые могут быть уже прикреплены к сеансу:
public void refreshDetached(T entity, Long id)
{
// Check for any OTHER instances already attached to the session since
// refresh will not work if there are any.
T attached = (T) session.load(getPersistentClass(), id);
if (attached != entity)
{
session.evict(attached);
session.lock(entity, LockMode.NONE);
}
session.refresh(entity);
}
Возможно, он ведет себя немного по-другому на Eclipselink. Чтобы повторно присоединить отдельные объекты без получения устаревших данных, я обычно делаю:
Object obj = em.find(obj.getClass(), id);
и в качестве необязательного второго шага (чтобы сделать кэши недействительными):
em.refresh(obj)
Извините, не могу добавить комментарии (пока?).
Использование Hibernate 3.5.0-Final
Тогда как Session#lock
Метод это устарело, Javadoc предлагает использовать Session#buildLockRequest(LockOptions)#lock(entity)
и если вы убедитесь, что ваши ассоциации имеют cascade=lock
ленивая загрузка тоже не проблема.
Итак, мой метод присоединения выглядит как
MyEntity attach(MyEntity entity) {
if(getSession().contains(entity)) return entity;
getSession().buildLockRequest(LockOptions.NONE).lock(entity);
return entity;
Первоначальные тесты показывают, что это работает удовольствие.
Попробуйте getHibernateTemplate().replicate(entity,ReplicationMode.LATEST_VERSION)
Чтобы присоединить этот объект, вы должны использовать merge();
этот метод принимает в параметре, что ваша сущность отсоединена и возвращает сущность, будет присоединена и загружена из базы данных.
Example :
Lot objAttach = em.merge(oldObjDetached);
objAttach.setEtat(...);
em.persist(objAttach);
В оригинальном посте есть два метода, update(obj)
а также merge(obj)
которые упоминаются, чтобы работать, но в противоположных обстоятельствах. Если это действительно так, то почему бы не проверить, находится ли объект уже в сеансе, а затем вызвать update(obj)
если это так, в противном случае позвоните merge(obj)
,
Тест на существование в сессии session.contains(obj)
, Поэтому я думаю, что будет работать следующий псевдокод:
if (session.contains(obj))
{
session.update(obj);
}
else
{
session.merge(obj);
}
Свойство hibernate.allow_refresh_detached_entity
сделал трюк для меня. Но это общее правило, поэтому оно не очень подходит, если вы хотите делать это только в некоторых случаях. Я надеюсь, что это помогает.
Проверено в Hibernate 5.4.9
Сначала вызывается merge() (для обновления постоянного экземпляра), а затем блокировка (LockMode.NONE) (для присоединения текущего экземпляра, а не того, который возвращается с помощью merge()), похоже, работает для некоторых случаев использования.
Hibernate поддерживает повторное подключение отсоединенного объекта сервалами, см. Руководство пользователя Hibernate.