Как правильно повторно прикрепить отдельные объекты в Hibernate? - PullRequest
177 голосов
/ 27 мая 2009

У меня есть ситуация, в которой мне нужно повторно прикрепить отсоединенные объекты к сеансу гибернации, хотя в сеансе МОЖЕТ уже существовать объект с таким же идентификатором, что приведет к ошибкам.

Прямо сейчас я могу сделать одну из двух вещей.

  1. getHibernateTemplate().update( obj ) Это работает тогда и только тогда, когда объект не существует в сеансе гибернации. Выдаются исключения, указывающие, что объект с данным идентификатором уже существует в сеансе, когда он понадобится мне позже.

  2. getHibernateTemplate().merge( obj ) Это работает, если и только если объект существует в сеансе гибернации. Исключения генерируются, когда мне нужно, чтобы объект был в сеансе позже, если я использую это.

Учитывая эти два сценария, как я могу в общем случае присоединять сеансы к объектам? Я не хочу использовать исключения для управления потоком решения этой проблемы, так как должно быть более элегантное решение ...

Ответы [ 15 ]

172 голосов
/ 14 декабря 2010

Таким образом, кажется, что в JPA нет способа присоединить устаревшую отдельную сущность.

merge() переведет устаревшее состояние в БД, и перезаписать все промежуточные обновления.

refresh() не может быть вызвано на отдельном объекте.

lock() нельзя вызвать на отдельном объекте, и даже если бы он мог, и он снова прикрепил объект, вызов 'lock' с аргументом 'LockMode.NONE' подразумевая, что вы блокируете, но не блокируете, это самая нелогичная часть дизайна API, которую я когда-либо видел.

Итак, вы застряли. Есть метод detach(), но нет attach() или reattach(). Очевидный шаг в жизненном цикле объекта вам недоступен.

Судя по количеству похожих вопросов о JPA, кажется, что даже если JPA действительно утверждает, что имеет согласованную модель, это, безусловно, не соответствует ментальной модели большинства программистов, которые были прокляты тратить много часов, пытаясь понять как заставить JPA делать самые простые вещи и получить кеш Код управления во всех их приложениях.

Кажется, единственный способ сделать это - сбросить свою несвежую отстраненную сущность и сделайте запрос на поиск с тем же идентификатором, который попадет в L2 или DB.

Mik

25 голосов
/ 18 мая 2012

Во всех этих ответах отсутствует важное различие. update () используется для (повторного) присоединения вашего графа объектов к сеансу. Объекты, которые вы передаете, это те, которые сделаны управляемыми.

merge () на самом деле не является (пере) приложением API. Заметьте, что merge () имеет возвращаемое значение? Это потому, что он возвращает вам управляемый граф, который может не быть графом, который вы передали. merge () - это JPA API, и его поведение регулируется спецификацией 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 () вместо этого ...

19 голосов
/ 29 мая 2009

Недипломатический ответ: Возможно, вы ищете расширенный контекст персистентности. Это одна из главных причин Seam Framework ... Если вы, в частности, пытаетесь использовать Hibernate в Spring, посмотрите этот кусок документации Seam.

Дипломатический ответ: Это описано в документах Hibernate . Если вам нужно больше разъяснений, взгляните на Раздел 9.3.2 Сохранение Java с Hibernate под названием «Работа с отделенными объектами». Я бы настоятельно рекомендовал бы вам получить эту книгу, если вы делаете что-то большее, чем CRUD с Hibernate.

12 голосов
/ 10 сентября 2010

Если вы уверены, что ваша сущность не была изменена (или если вы согласны, что любая модификация будет потеряна), то вы можете снова присоединить ее к сеансу с блокировкой.

session.lock(entity, LockMode.NONE);

Он ничего не заблокирует, но получит объект из кэша сеанса или (если его там не найден) прочитает его из БД.

Очень полезно предотвращать LazyInitException, когда вы перемещаетесь по отношениям из "старых" (например, из HttpSession) сущностей. Сначала вы заново «присоединяете» сущность.

Использование get также может работать, кроме случаев, когда вы получаете отображение наследования (которое уже вызывает исключение в getId ()).

entity = session.get(entity.getClass(), entity.getId());
9 голосов
/ 15 декабря 2012

Я вернулся к 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() не следует использовать сразу, и в зависимости от функциональных требований можно выбрать один или несколько из них.

7 голосов
/ 21 августа 2012

Я сделал это в 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 сравнивает объекты по идентификатору и типу базы данных. Contains использует метод 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;
}
4 голосов
/ 15 июля 2014

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, которые мне нравятся, различные опции присоединения, касающиеся измененных сущностей и их свойств.

3 голосов
/ 18 мая 2012

Я предложил решение «обновить» объект из постоянного хранилища, который будет учитывать другие объекты, которые могут быть уже присоединены к сеансу:

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);
}
2 голосов
/ 25 октября 2015

Возможно, он ведет себя немного по-другому на Eclipselink. Чтобы повторно прикрепить отсоединенные объекты без получения устаревших данных, я обычно делаю:

Object obj = em.find(obj.getClass(), id);

и в качестве необязательного второго шага (чтобы сделать кэши недействительными):

em.refresh(obj)
2 голосов
/ 24 ноября 2013

Извините, не могу добавить комментарии (пока?).

Использование 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;

Первоначальные тесты показывают, что это работает удовольствие.

...