Как правильно повторно прикрепить отдельные объекты в Hibernate?

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

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

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

  2. 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

SessionFactoryOptionsBuilder

Сначала вызывается merge() (для обновления постоянного экземпляра), а затем блокировка (LockMode.NONE) (для присоединения текущего экземпляра, а не того, который возвращается с помощью merge()), похоже, работает для некоторых случаев использования.

Hibernate поддерживает повторное подключение отсоединенного объекта сервалами, см. Руководство пользователя Hibernate.

try getHibernateTemplate().saveOrUpdate()
Другие вопросы по тегам