Hibernateでデタッチされたオブジェクトを再アタッチする適切な方法は何ですか?


186

同じIDのオブジェクトがすでにセッションに存在している可能性があるにもかかわらず、切り離されたオブジェクトを休止状態のセッションに再アタッチする必要がある状況があり、エラーが発生します。

現在、私は2つのことの1つを行うことができます。

  1. getHibernateTemplate().update( obj ) これは、オブジェクトが休止状態のセッションにまだ存在しない場合にのみ機能します。後で必要になったときに、指定した識別子のオブジェクトがすでにセッションに存在しているという例外がスローされます。

  2. getHibernateTemplate().merge( obj ) これは、オブジェクトが休止状態のセッションに存在する場合にのみ機能します。これを使用すると、オブジェクトが後でセッションに入る必要があるときに例外がスローされます。

これら2つのシナリオを前提として、一般的にセッションをオブジェクトにアタッチするにはどうすればよいですか?よりエレガントな解決策があるはずなので、この問題の解決策のフローを制御するために例外を使用したくありません...

回答:


181

そのため、JPAで古い切り離されたエンティティを再接続する方法はないようです。

merge() 古い状態をDBにプッシュし、間にある更新を上書きします。

refresh() デタッチされたエンティティで呼び出すことはできません。

lock() デタッチされたエンティティで呼び出すことはできず、それが可能で、エンティティを再アタッチした場合、引数 'LockMode.NONE'で 'lock'を呼び出すことは、ロックしているがロックしていないことを意味し、API設計の最も直感に反する部分です私が今まで見てきました。

だからあなたは行き​​詰まっています。ありますdetach()方法は、ありませんattach()reattach()。オブジェクトのライフサイクルの明確なステップは利用できません。

JPAに関する類似の質問の数から判断すると、JPAが一貫したモデルを持っていると主張しても、取得方法を理解するために何時間も浪費するように呪われてきたほとんどのプログラマーのメンタルモデルと一致しないようです。 JPAは、最も単純なことを実行し、アプリケーション全体でキャッシュ管理コードを作成します。

それを行う唯一の方法は、古くて切り離されたエンティティを破棄し、L2またはDBにヒットする同じIDで検索クエリを実行することです。

ミク


1
JPA仕様がrefresh()分離エンティティで許可されていない理由があるのでしょうか。2.0仕様を調べても、正当な理由はありません。許可されていないというだけです。
FGreg 2013年

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 '26

永続オブジェクトはうまく機能します。ダーティフラグは、初期ロードとflush()時の値の間のデルタに基づいて設定されます。切り離されたオブジェクトには必要であり、現在この機能はありません。hibernateがそれを行う方法は、デタッチされたオブジェクトに追加のハッシュ/ IDを追加することです。また、永続オブジェクトの場合と同じように、デタッチされたオブジェクトの最後の状態のスナップショットを保持します。そのため、既存のコードをすべて活用して、デタッチされたオブジェクトに対して機能させることができます。この方法では、@ mikhailfrancoが「古い状態をDBにプッシュして、途中の更新を上書きする」ことはしないと述べています
tom

2
Hibernate 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)
Martin Vysny

32

これらの回答はすべて、重要な違いを見逃しています。update()は、オブジェクトグラフをセッションに(再)アタッチするために使用されます。あなたがそれを渡すオブジェクトは、管理されるものです。

merge()は実際には(再)アタッチメントAPIではありません。merge()に戻り値があることに注意してください。これはマネージドグラフを返すためです。これは、渡したグラフとは異なる場合があります。merge()はJPA APIであり、その動作はJPA仕様に準拠しています。merge()に渡したオブジェクトがすでに管理されている(すでにセッションに関連付けられている)場合、それはHibernateが動作するグラフです。渡されたオブジェクトは、merge()から返されたオブジェクトと同じです。ただし、me​​rge()に渡すオブジェクトがデタッチされている場合、Hibernateは管理対象の新しいオブジェクトグラフを作成し、デタッチされたグラフから新しい管理対象のグラフに状態をコピーします。繰り返しますが、これはすべてJPA仕様によって規定および管理されています。

「このエンティティが管理されているか、管理されていることを確認する」ための一般的な戦略に関しては、まだ挿入されていないデータも考慮に入れるかどうかによって異なります。あなたがそうすると仮定すると、次のようなものを使用します

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

update()ではなくsaveOrUpdate()を使用したことに注意してください。まだ挿入されていないデータをここで処理したくない場合は、代わりにupdate()を使用してください...


3
これがこの質問に対する正しい答えです-ケースはクローズされました!
cwash、2014

2
Session.contains(Object)参照によってチェックします。セッションで同じ行を表す別のエンティティがすでに存在し、分離されたインスタンスを渡すと、例外が発生します。
djmj 14

Session.contains(Object)存在する場合は参照することによりチェックし、別のセッションで同じ行を表すエンティティが、それは偽を返すウィル、そしてそれを更新します。
AxelWass

19

非外交的回答:拡張された永続化コンテキストを探している可能性があります。これがSeam Frameworkの背後にある主な理由の1つです。特にSpringでHibernateを使用するのに苦労している場合は、この Seamのドキュメントをチェックしてください。

外交的回答:これはHibernate docsで説明されています。さらに説明が必要な場合は、「分離オブジェクトの操作」と呼ばれるHibernateでのJava Persistenceのセクション9.3.2を参照してください。私は思い強くあなたがHibernateでCRUD以上の何かをやっている場合は、この本を入手をお勧めします。


5
seamframework.orgから:「Seam 3のアクティブな開発はRed Hatによって中止されました。」リンク「このSeamのドキュメントの一部」も死んでいます。
badbishop 2018年

14

エンティティが変更されていないと確信している場合(または変更が失われることに同意した場合)、ロックを使用してセッションに再アタッチできます。

session.lock(entity, LockMode.NONE);

何もロックしませんが、エンティティをセッションキャッシュから取得するか、(そこに見つからない場合は)DBから読み取ります。

「古い」(たとえばHttpSessionからの)エンティティから関係をナビゲートするときにLazyInitExceptionを防ぐことは非常に便利です。最初にエンティティを「再接続」します。

継承をマッピングする場合(getId()で既に例外がスローされます)を除いて、getを使用することもできます。

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

2
エンティティをセッションに再度関連付けたいと思います。残念ながら、Session.lock(entity, LockMode.NONE)例外が発生して失敗します:初期化されていない一時的なコレクションを再度関連付けることができませんでした。どうすればこれを克服できますか?
dma_k

1
実際、私は完全に正しくはありませんでした。lock()を使用すると、エンティティに再アタッチされますが、バインドされている他のエンティティには再アタッチされません。したがって、entity.getOtherEntity()。getYetAnotherEntity()を実行すると、LazyInit例外が発生する可能性があります。それを克服するために私が知っている唯一の方法は、findを使用することです。entity = em.find(entity.getClass()、entity.getId();
John Rizzo

Session.find()APIメソッドはありません。多分意味しSession.load(Object object, Serializable id)ます。
dma_k

11

これは非常に一般的な質問であるため、 この回答に基づいてこの記事を作成しました。

エンティティの状態

JPAは以下のエンティティー状態を定義します。

新規(一時的)

Hibernate Session(aka Persistence Context)に関連付けられておらず、データベーステーブル行にマップされていない、新しく作成されたオブジェクトは、新規(一時的)状態と見なされます。

永続化するには、EntityManager#persistメソッドを明示的に呼び出すか、推移的永続化メカニズムを利用する必要があります。

永続的(管理)

永続エンティティがデータベーステーブル行に関連付けられており、現在実行中の永続コンテキストによって管理されています。このようなエンティティに加えられた変更はすべて検出され、データベースに伝達されます(セッションのフラッシュ時)。

Hibernateを使用すると、INSERT / UPDATE / DELETEステートメントを実行する必要がなくなります。Hibernateはトランザクション後書き作業スタイルを採用しており、変更は現在のSessionフラッシュ時間中の最後の責任ある瞬間に同期されます。

戸建

現在実行中の永続コンテキストが閉じられると、以前に管理されていたすべてのエンティティが切り離されます。継続的な変更は追跡されなくなり、自動データベース同期は行われません。

エンティティの状態遷移

EntityManagerインターフェイスで定義されたさまざまなメソッドを使用してエンティティの状態を変更できます。

JPAエンティティの状態遷移をよりよく理解するには、次の図を検討してください。

JPAエンティティーの状態遷移

JPAを使用する場合、分離されたエンティティをアクティブなに再度関連付けるにはEntityManagerマージ操作を使用できます。

merge次の図に示すように、ネイティブHibernate APIを使用する場合は、とは別に、更新メソッドを使用して、切り離されたエンティティをアクティブなHibernateセッションに再接続できます。

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中に、管理対象エンティティが変更されたことをダーティチェックメカニズムが検出すると、UPDATEが生成されます。

そのため、を使用するmergeと、デタッチされたオブジェクトインスタンスは、マージ操作後もデタッチされたままになります。

切り離されたエンティティの再接続

JPAはHibernateをサポートしていますが、updateメソッドによる再接続をサポートしていません。

Hibernate Sessionは特定のデータベース行に1つのエンティティオブジェクトのみを関連付けることができます。これは、永続コンテキストがメモリ内キャッシュ(1次レベルキャッシュ)として機能し、1つの値(エンティティ)のみが特定のキー(エンティティタイプとデータベース識別子)に関連付けられているためです。

エンティティを再接続できるのは、現在のHibernateにすでに関連付けられている(同じデータベース行に一致する)他の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ロードされた状態をフェッチするSELECTステートメントをトリガーするHibernateアノテーションを使用します。これは、ダーティチェックメカニズムによって使用されます。

@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {

    //Code omitted for brevity
}

NonUniqueObjectExceptionに注意してください

で発生する可能性のある問題の1つupdateは、次の例のように、永続コンテキストに同じIDで同じタイプのエンティティ参照がすでに含まれている場合です。

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これは、2番目に、渡したものと同じ識別子を持つエンティティがEntityManager既に含まれており、永続コンテキストが同じエンティティの2つの表現を保持できないためです。Bookupdate

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、フラッシュの直前にそれが問題にならないか(Hibernateがフラッシュの前に1回の呼び出しですべての注釈付きエンティティをフェッチしたかどうかが問題になる可能性があります)?
Andronicus

@SelectBeforeUpdate永続性コンテキストの中にSELECTをトリガーflush操作。詳細については、のgetDatabaseSnapshotメソッドをDefaultFlushEntityEventListener確認しください。
Vlad Mihalcea

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()そのまま使用すべきではなく、機能要件に基づいてそれらの1つ以上を選択できます。


7

私はNHibernateを使用してC#でその方法でそれを行いましたが、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は常にfalseだったため、すべてのオブジェクトでFirst Lockが呼び出されました。問題は、NHibernateがデータベースIDとタイプによってオブジェクトを比較することです。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

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の数少ない側面の1つです。変更されたエンティティとそのプロパティに関するさまざまなアタッチオプションです。


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

コメントを追加できないようです(まだ?)。

Hibernate 3.5.0-Finalの使用

これはSession#lock非推奨のメソッドですが、javadoc 使用を推奨Session#buildLockRequest(LockOptions)#lock(entity)しています。関連付けが確実に存在する場合cascade=lockは、遅延読み込みも問題になりません。

だから、私のattachメソッドは少し似ています

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);

オプションとして2番目のステップ(キャッシュを無効にするため):

em.refresh(obj)


1

元の投稿では2つの方法がupdate(obj)ありmerge(obj)、それらは機能すると言われていますが、逆の状況です。これが本当に当てはまる場合は、最初にオブジェクトがすでにセッションにあるかどうかをテストしてからupdate(obj)、そうである場合はを呼び出し、そうでない場合はを呼び出してくださいmerge(obj)

セッションでの存在のテストはsession.contains(obj)です。したがって、次の疑似コードが機能すると思います。

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

2
contains()は参照による比較をチェックしますが、Hibernate関数はデータベースIDによって機能します。セッションでsession.mergeが呼び出されることはありません。
Verena Haunschmid 2012

1

このオブジェクトを再アタッチするには、merge();を使用する必要があります。

このメソッドは、エンティティをデタッチしてパラメーターに受け入れ、エンティティをアタッチしてデータベースから再読み込みします。

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

0

最初にmerge()(永続的なインスタンスを更新するため)を呼び出し、次にlock(LockMode.NONE)(merge()によって返されたインスタンスではなく、現在のインスタンスを接続するため)を呼び出す)は、いくつかのユースケースで機能するようです。


0

プロパティhibernate.allow_refresh_detached_entityは私のためにトリックをしました。ただし、これは一般的なルールであるため、一部のケースでのみ実行する場合はあまり適していません。お役に立てば幸いです。

Hibernate 5.4.9でテスト済み

SessionFactoryOptionsBuilder



弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.