Який правильний спосіб повторного прикріплення відокремлених об’єктів у сплячому режимі?


186

У мене ситуація, коли мені потрібно знову приєднати відокремлені об’єкти до сплячого сеансу, хоча об’єкт тієї самої ідентичності МОЖЕ вже існувати в сеансі, що спричинить помилки.

Зараз я можу зробити одне з двох речей.

  1. getHibernateTemplate().update( obj ) Це працює, якщо і лише в тому випадку, якщо об'єкт ще не існує в сплячому режимі. Винятки викидаються, заявляючи, що об'єкт із даним ідентифікатором вже існує в сеансі, коли мені це потрібно пізніше.

  2. getHibernateTemplate().merge( obj ) Це працює, якщо і лише якщо об’єкт існує у сплячому режимі. Винятки викидаються, коли мені потрібно, щоб об'єкт був на сеансі пізніше, якщо я цим користуюся.

З огляду на ці два сценарії, як я можу загально приєднати сеанси до об’єктів? Я не хочу використовувати винятки для контролю потоку рішення цієї проблеми, оскільки повинно бути більш елегантне рішення ...

Відповіді:


181

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

merge() підштовхне старий стан до БД та замінить будь-які втручаються оновлення.

refresh() не можна викликати відокремлену сутність.

lock() не можна викликати відокремлену сутність, і навіть якщо вона могла, і вона повторно приєднала сутність, викликаючи "замок" аргументом "LockMode.NONE", що означає, що ви блокуєте, але не блокуєте, є найбільш протизаконною частиною дизайну API Я коли-небудь бачив.

Отже, ви застрягли. Існує detach()метод, але немає attach()або reattach(). Очевидний крок у життєвому циклі об'єкта вам недоступний.

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

Здається, єдиний спосіб зробити це - відмовитись від застарілих відокремлених об'єктів і знайти запит з тим самим ідентифікатором, який потрапить до L2 або БД.

Мік


1
Цікаво, чи є причина, яку специфіка JPA не дозволяє refresh()для відокремлених організацій? Переглядаючи специфікацію 2.0, я не бачу жодного виправдання; тільки що це не дозволено.
FGreg

11
Це, безумовно, НЕ точне. З JPwH: *Reattaching a modified detached instance* A detached instance may be reattached to a new Session (and managed by this new persistence context) by calling update() on the detached object. In our experience, it may be easier for you to understand the following code if you rename the update() method in your mind to reattach()—however, there is a good reason it’s called updating.Докладніше можна знайти у розділі 9.3.2
cwash

Стійкі об'єкти працюють чудово, брудний прапор встановлюється на основі дельти між початковим навантаженням і значенням (ими) в час (змивання). Окремі об’єкти потребують і наразі не мають цієї функції. Спосіб, який робить це в сплячку, - це додати додатковий хеш-код для відокремлених об'єктів. І зберігайте доступний знімок останнього стану відокремленого об'єкта, як і для стійких об'єктів. Таким чином вони можуть використовувати весь наявний код і змусити його працювати для відокремлених об'єктів. Таким чином , як @mikhailfranco зазначив , що ми не будемо «штовхати несвіже стан в БД, і перезаписати всі проміжні оновлення»
Том

2
За словами сплячого javadoc (але не JPA), lock(LockMode.NONE)насправді його можна викликати тимчасовим об'єктом, і він повторно приєднає сутність до сесії. Див stackoverflow.com/a/3683370/14379
seanf

Блокування не працювало для мене: java.lang.IllegalArgumentException: сутність не в контексті стійкості на org.hibernate.internal.SessionImpl.lock (SessionImpl.java:3491) на org.hibernate.internal.SessionImpl.lock (SessionImpl. java: 3482) в com.github.vok.framework.DisableTransactionControlEMDelegate.lock (DB.kt)
Мартін Висний,

32

Усі ці відповіді пропускають важливу відмінність. update () використовується для (повторного) приєднання вашого об'єктного графіка до сесії. Об'єкти, яким ви передаєте його, - це ті, які зроблені керованими.

merge () насправді не є API (повторного) вкладення. Помітьте, що merge () має значення повернення? Це тому, що він повертає вам керований графік, який може бути не графіком, яким ви його передали. merge () - API JPA, і його поведінка регулюється специфікацією JPA. Якщо об’єкт, з яким ви передаєте для об'єднання (), вже керується (вже асоціюється з Сесією), то це графік, з яким працює Hibernate; переданий об'єкт - це той самий об'єкт, який повертається з об'єднання (). Якщо, проте, об'єкт, який ви передаєте в об'єднання (), від'єднаний, Hibernate створює новий керований графік об'єкта, який керується, і він копіює стан з вашого окремого графа на новий керований графік. Знову ж таки, це все продиктовано та регулюється специфікацією JPA.

З точки зору загальної стратегії "переконайтеся, що цим об'єктом керують, або зробити його керованим", це залежить від того, чи потрібно також враховувати ще не вставлені дані. Якщо припустити, що ви робите, використовуйте щось подібне

if ( session.contains( myEntity ) ) {
    // nothing to do... myEntity is already associated with the session
}
else {
    session.saveOrUpdate( myEntity );
}

Зауважте, що я використовував saveOrUpdate (), а не update (). Якщо ви не хочете, щоб тут ще не вставлені дані, використовуйте натомість update () ...


3
Це правильна відповідь на це питання - справа закрита!
перемолоти

2
Session.contains(Object)чеки за посиланням. Якщо в сеансі вже є інша сутність, яка представляє той самий рядок, і ви передасте відокремлений екземпляр, ви отримаєте виняток.
djmj

Як Session.contains(Object)перевірка за посиланням, якщо є інша сутність, що представляє той самий рядок у сеансі, вона поверне помилкову, і вона оновить її.
AxelWass

19

Недипломатична відповідь: Ви, мабуть, шукаєте розширений контекст стійкості. Це одна з головних причин, що стоять за рамками швів ... Якщо ви ведете важкі зусилля, щоб використати сплячку навесні, зокрема, ознайомтеся з цим документом шва.

Дипломатична відповідь: Це описано в документах зі сну . Якщо вам потрібні додаткові роз'яснення, перегляньте Розділ 9.3.2 Наполегливості Java зі сплячим режимом під назвою "Робота з окремими об'єктами". Я настійно рекомендую вам отримати цю книгу, якщо ви робите щось більше, ніж CRUD зі сплячим режимом.


5
З сайту seamframework.org : "Активна розробка шва 3 була зупинена Red Hat". Посилання "цей фрагмент документів Шва" також мертве.
Badbishop

14

Якщо ви впевнені, що ваша організація не була змінена (або ви погоджуєтесь, що будь-яка модифікація буде втрачена), ви можете повторно приєднати її до сесії з блокуванням.

session.lock(entity, LockMode.NONE);

Він нічого не заблокує, але він отримає об'єкт із кеш-сеансу або (якщо його там не знайдено) прочитати з БД.

Дуже корисно запобігти LazyInitException, коли ви переглядаєте відносини зі "старих" (наприклад, з HttpSession) сутностей. Ви спочатку "повторно приєднаєте" сутність.

Використання get може також працювати, за винятком випадків, коли ви отримуєте спадкове відображення (що вже викине виняток на getId ()).

entity = session.get(entity.getClass(), entity.getId());

2
Я хотів би відновити об'єкт із сеансом. На жаль, Session.lock(entity, LockMode.NONE)не вдається за винятком сказати: не вдалося зв'язати неініціалізований перехідний збір. Як це можна подолати?
dma_k

1
Насправді я був не зовсім правий. Використовуючи lock () повторно прив'язує вашу сутність, але не інші об'єкти, пов'язані з нею. Отже, якщо ви робите entit.getOtherEntity (). GetYetAbodyEntity (), у вас може бути виняток LazyInit. Єдиний спосіб, який я знаю, щоб подолати, це використовувати пошук. entitet = em.find (entit.getClass (), entit.getId ();
Джон Ріццо

Не існує Session.find()методу API. Можливо, ти маєш на увазі Session.load(Object object, Serializable id).
dma_k

11

Оскільки це дуже поширене питання, я написав цю статтю , на якій ґрунтується ця відповідь.

Сутні держави

JPA визначає наступні сутності:

Нове (тимчасове)

Новостворений об’єкт, який ніколи не асоціювався зі сплячим режимом Session(ака Persistence Context) і не відображається в жодному рядку таблиці баз даних, вважається в новому (перехідному) стані.

Щоб стати наполегливим, нам потрібно або явно викликати EntityManager#persistметод, або скористатися механізмом перехідної стійкості.

Постійний (керований)

Послідовна сутність була пов'язана з рядком таблиці баз даних, і нею керує поточний контекст постійності. Будь-які зміни, внесені до такої сутності, будуть виявлені та розповсюджені до бази даних (під час часу сесії).

У режимі Hibernate нам більше не потрібно виконувати заяви INSERT / UPDATE / DELETE. У сплячому режимі використовується стиль роботи транзакційного запису, і зміни синхронізуються в самий останній відповідальний момент, під час поточного Sessionпотоку.

Окремі

Після закриття поточного контексту постійності всі раніше керовані об'єкти відключаються. Послідовні зміни більше не відстежуються, і автоматична синхронізація бази даних не відбуватиметься.

Переходи сутності держави

Ви можете змінити стан сутності за допомогою різних методів, визначених EntityManagerінтерфейсом.

Щоб краще зрозуміти переходи стану сутності JPA, врахуйте наступну схему:

Переходи стану сутності JPA

Використовуючи JPA, щоб пов’язати відокремлену сутність з активною EntityManager, ви можете використовувати операцію злиття .

При використанні нативного API Hibernate, крім merge, ви можете повторно приєднати відокремлену сутність до активної гібернатової сесії, використовуючи методи оновлення, як показано на наступній схемі:

Переходи в стан сплячої сутності

Об'єднання відокремленої сутності

Злиття збирається скопіювати відокремлений стан об'єкта (джерело) в керований екземпляр об'єкта (призначення).

Подумайте, що ми зберігаємо наступну 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відокремлений екземпляр об'єкта буде продовжувати залишатися відокремленим навіть після операції злиття.

Повторне з'єднання відокремленої сутності

Перебуває в сплячому режимі, але не JPA підтримує повторне приєднання за допомогою updateметоду.

Hibernate Sessionможе асоціювати лише один об'єкт сутності для заданого рядка бази даних. Це пояснюється тим, що контекст стійкості діє як кеш пам'яті (кеш першого рівня), і лише одне значення (сутність) пов'язане з заданим ключем (тип сутності та ідентифікатор бази даних).

Суб'єкт можна повторно приєднати, лише якщо немає жодного іншого об'єкта JVM (відповідного тому ж рядку бази даних), який вже пов'язаний з поточним режимом сплячого режиму 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Метод вимагає , щоб ви в сплячий режим .unwrapEntityManagerSession

На відміну від цього 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операцією, тому скорочує час виконання пакетного оновлення.


Гарна відповідь. Мені все-таки цікаво про @SelectBeforeUpdateанотацію. Коли спрацьовує вибір? Під час дзвінка updateбезпосередньо перед спалахуванням чи це насправді не має значення (могло б мати значення, якщо сплячий за один дзвінок перед початком спалаху дістав усі помічені об’єкти)?
Андронік

@SelectBeforeUpdateЗапускає ВИБРАТИ під час контексту Persistence flushоперації. Перевірте на getDatabaseSnapshotметод вDefaultFlushEntityEventListener для більш докладної інформації.
Влад Міхалча

10

Я повернувся до 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

Я це робив так у 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();
        }
    }
}

Перший замок був викликаний на кожному об'єкті, оскільки "Містить" завжди був помилковим. Проблема полягає в тому, що 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;
}

4

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

Я придумав рішення "оновити" об'єкт із магазину збереження, який враховуватиме інші об'єкти, які вже можуть бути приєднані до сеансу:

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

На жаль, не можна додавати коментарі (поки що).

Використання сплячого режиму 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;

Початкові тести дозволяють припустити, що це працює як задоволення.


2

Можливо, він поводиться дещо інакше на Eclipselink. Для повторного прикріплення відокремлених об'єктів, не отримуючи застарілих даних, зазвичай роблю:

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

і як необов'язковий другий крок (щоб визнати недійсними кеші):

em.refresh(obj)


1

У вихідному пості, є два способи, update(obj)і merge(obj)що згадується в роботу, але в протилежних обставин. Якщо це дійсно так, то чому б не перевірити, чи спочатку об’єкт вже знаходиться в сеансі, а потім зателефонувати, update(obj)якщо він є, інакше зателефонуйте merge(obj).

Тест на існування в сесії є session.contains(obj). Тому я думаю, що працює наступний псевдо-код:

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}

2
містить () чеки, порівнює за посиланням, але сплячі функції працюють за ідентифікатором бази даних. session.merge ніколи не буде викликаний у ваш код.
Верена Хауншмід

1

щоб повторно приєднати цей об’єкт, ви повинні використовувати merge ();

цей метод прийме в параметрі, що відключається та повертає сутність, буде додано та перезавантажено з бази даних.

Example :
    Lot objAttach = em.merge(oldObjDetached);
    objAttach.setEtat(...);
    em.persist(objAttach);

0

виклик спочатку merge () (для оновлення стійкого екземпляра), потім блокування (LockMode.NONE) (для приєднання поточного екземпляра, а не того, який повертається шляхом merge ()), здається, працює для деяких випадків використання.


0

Власність hibernate.allow_refresh_detached_entity зробила для мене трюк. Але це загальне правило, тому воно не дуже підходить, якщо ви хочете це робити лише в деяких випадках. Я сподіваюся, що це допомагає.

Випробувано на сплячому стані 5.4.9

СесіяFactoryOptionsBuilder



Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.