PersistentObjectException:JPAおよびHibernateによってスローされた永続化に渡された分離エンティティ


237

私は、多対1の関係が含まれているJPA永続化オブジェクトモデルを持っている:Account多くを持っていますTransactions。あTransactionに1つありAccountます。

コードのスニペットは次のとおりです。

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

Accountオブジェクトを作成し、トランザクションを追加して、Accountオブジェクトを正しく永続化できます。しかし、トランザクションを作成するときは、既存の永続化されたアカウントを使用し、トランザクションを永続化します、私は例外を取得します:

原因:org.hibernate.PersistentObjectException:永続化のために渡された分離エンティティ:com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141)

したがって、Accountトランザクションを含むを永続化できますが、を含むトランザクションは永続化できませんAccount。これはAccount添付されていない可能性があるためだと思っていましたが、このコードでも同じ例外が発生します。

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

Transactionすでに永続化されているAccountオブジェクトに関連付けられているを正しく保存するにはどうすればよいですか?


15
私の場合、エンティティマネージャーを使用して永続化しようとしたエンティティのIDを設定していました。idのセッターを削除すると、正常に動作し始めました。
Rushi Shah

私の場合、IDを設定していませんでしたが、同じアカウントを使用している2人のユーザーがいて、そのうちの1人が(正しく)エンティティを永続化し、後者が同じエンティティを永続化しようとしたときにエラーが発生しました。持続した。
sergioFC

回答:


129

これは典型的な双方向の一貫性の問題です。このリンクだけでなく、このリンクでもよく説明されいます

前の2つのリンクの記事に従って、双方向関係の両側でセッターを修正する必要があります。片側のセッターの例はこのリンクにあります。

多側のセッターの例はこのリンクにあります。

セッターを修正したら、エンティティアクセスタイプを「プロパティ」として宣言します。「プロパティ」アクセスタイプを宣言するベストプラクティスは、すべての注釈をメンバープロパティから対応するゲッターに移動することです。エンティティクラス内で「フィールド」と「プロパティ」のアクセスタイプを混在させることは重要ではありません。そうでない場合、動作はJSR-317仕様では定義されていません。


2
ps:@Idアノテーションは、hibernateがアクセスタイプを識別するために使用するアノテーションです。
ディエゴプレンツ2015年

2
例外は次のとおりです。detached entity passed to persistなぜ一貫性を改善すると機能するのですか?整合性は修復されましたが、オブジェクトはまだ分離されています。
Gilgamesz

1
@Sam、説明ありがとうございます。しかし、まだわかりません。「特別な」セッターがなければ、双方向の関係は満足されないことがわかります。しかし、オブジェクトがデタッチされた理由はわかりません。
Gilgamesz

28
リンクのみで回答する回答は投稿しないでください。
El Mac、

6
この回答が質問とどのように関連しているかはまったくわかりませんか?
Eugen Labun

271

解決策は簡単です。CascadeType.MERGE代わりにCascadeType.PERSISTまたはを使用してくださいCascadeType.ALL

私は同じ問題をCascadeType.MERGE抱えており、私のために働いています。

並べ替えてください。


5
驚くべきことに、それも私のために働いた。CascadeType.ALLには他のすべてのカスケードタイプが含まれているので意味がありません... WTF?Spring 4.0.4、Spring Data JPA 1.8.0、Hibernate 4.Xを使用しています。ALLが機能しないのはなぜですか?
Vadim Kirilchuk、2015年

22
@VadimKirilchukこれは私にとってもうまくいき、それは完全に理にかなっています。TransactionはPERSISTEDであるため、PERSISTアカウントも試行しますが、アカウントはすでにdbにあるため、機能しません。ただし、CascadeType.MERGEを使用すると、代わりにアカウントが自動的にマージされます。
ガンスリンガー

4
これは、トランザクションを使用しない場合に発生する可能性があります。
lanoxx

1
別の解決策:すでに永続化されているオブジェクトを挿入しないようにしてください:)
Andrii Plotnikov

3
男ありがとう 参照キーをNOT NULLにする制限がある場合、永続化オブジェクトの挿入を回避することはできません。したがって、これが唯一の解決策です。ありがとうございました。
makkasi

22

子エンティティからカスケードを削除しTransactionます。

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

FetchType.EAGERは削除可能で、のデフォルトです@ManyToOne

それで全部です!

どうして?子エンティティで "cascade ALL"と言うTransactionと、すべてのDB操作が親エンティティに伝達される必要がありますAccount。そうするとpersist(transaction)persist(account)も呼び出されます。

ただし、一時的な(新しい)エンティティのみがpersistTransactionこの場合)に渡されます。切り離された(または他の非一時的な状態)ものは(Accountこの場合、すでにDBにあるため)場合があります。

したがって、「永続化のために渡された分離エンティティ」という例外が発生します。Accountエンティティが意味しています!Transactionあなたが呼ぶのpersistではありません。


通常、子から親に伝播する必要はありません。残念ながら、本には(良いものでも)ネットを通じて多くのコード例があり、まさにそれを行っています。なぜだかわかりません...たぶん、たまに何も考えずに単純に何度も何度もコピーされてしまうかもしれません...

お電話の場合は何が起こるかを推測remove(transaction)まだその@ManyToOneに「カスケードALL」を有しますか?account(ところで、他のすべてのトランザクションで!)にもDBから削除されます。しかし、それはあなたの意図ではありませんでしたね?


追加したいだけの場合、意図が本当に親と一緒に子を保存することであり、また、person(parent)とaddress(child)がDBによって自動生成されたaddressIdのように、子と一緒に親を削除する場合は、Personでsaveを呼び出す前に、トランザクションメソッドでアドレスを保存する呼び出し。この方法では、DBによって生成されたIDとともに保存されます。Hibenateは2つのクエリを実行するため、パフォーマンスへの影響はありません。クエリの順序を変更するだけです。
ヴィッキー

何も渡せない場合は、すべてのケースでデフォルト値を使用します。
Dhwanil Patel

15

マージの使用は危険で扱いにくいので、あなたの場合、それは汚い回避策です。少なくとも、エンティティオブジェクトを渡してマージすると、トランザクションへのアタッチが停止し、代わりにアタッチされた新しいエンティティが返されることを覚えておく必要があります。これは、誰かが古いエンティティオブジェクトをまだ所有している場合、それに対する変更は通知なく無視され、コミット時に破棄されることを意味します。

ここでは完全なコードを示していないため、トランザクションパターンを再確認することはできません。このような状況に陥る1つの方法は、マージと永続化を実行するときにアクティブなトランザクションがない場合です。その場合、永続性プロバイダーは、実行するすべてのJPA操作に対して新しいトランザクションを開き、呼び出しが戻る前にすぐにコミットして閉じることが期待されます。この場合、マージは最初のトランザクションで実行され、mergeメソッドが戻った後、トランザクションが完了して閉じ、返されたエンティティが切り離されます。その下の永続化は、2番目のトランザクションを開き、分離されたエンティティを参照しようとして例外を発生させます。何をしているのかよくわかっているのでなければ、常にトランザクション内でコードをラップしてください。

コンテナー管理トランザクションを使用すると、次のようになります。注:これは、メソッドがセッションBean内にあり、ローカルまたはリモートインターフェースを介して呼び出されることを前提としています。

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}

私は同じ問題に直面@Transaction(readonly=false)しています。サービス層でを使用しました。それでも同じ問題が発生します
Senthil Arumugam SP

なぜこのように機能するのかを完全に理解しているとは言えませんが、トランザクションアノテーション内に永続メソッドとビューからエンティティへのマッピングを配置することで問題が解決されました。
Deadron

これは実際に最も支持されているものよりも優れたソリューションです。
Aleksei Maide

13

おそらくこの場合はaccount、マージロジックを使用してオブジェクトを取得し、persist新しいオブジェクトを永続化するために使用されます。階層に既に永続化されたオブジェクトがある場合、メッセージが表示されます。このsaveOrUpdateような場合は、の代わりに使用してくださいpersist


3
それはJPAなので、類似のメソッドは.merge()だと思いますが、同じ例外が発生します。明確にするために、トランザクションは新しいオブジェクトですが、アカウントはそうではありません。
Paul Sanwald、2013年

使用@PaulSanwald mergetransactionあなたが同じエラーを取得したオブジェクト?
dan

実際、違う、間違って話した。.merge(transaction)の場合、トランザクションは永続化されません。
Paul Sanwald、

@PaulSanwaldうーん、それtransactionは持続されなかったのですか?どのように確認しましたか。mergeが永続化オブジェクトへの参照を返していることに注意してください。
dan

.merge()によって返されたオブジェクトのIDがnullです。また、後で.findAll()を実行していますが、オブジェクトがありません。
Paul Sanwald、2013年

12

id(pk)をpersistメソッドに渡さないでください。または、persist()ではなくsave()メソッドを試してください。


2
いいアドバイス!ただし、idが生成された場合のみ。割り当てられている場合は、通常、IDを設定します。
アルディアン、

これでうまくいきました。また、を使用TestEntityManager.persistAndFlush()してインスタンスを管理および永続化し、永続化コンテキストを基礎となるデータベースに同期させることもできます。元のソースエンティティを返します
Anyul Rivas 2018

7

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

問題を解決するには、次の手順を実行する必要があります。

1.子の関連付けのカスケードを削除する

そのため@CascadeType.ALL@ManyToOne関連付けからを削除する必要があります。子エンティティは、親の関連付けにカスケードしないでください。親エンティティのみが子エンティティにカスケードする必要があります。

@ManyToOne(fetch= FetchType.LAZY)

積極的なフェッチはパフォーマンスに非常に悪いため、fetch属性をに設定していることに注意してください。FetchType.LAZY

2.関連付けの両側を設定します

双方向の関連付けがある場合は常に、親エンティティのaddChildremoveChildメソッドを使用して両側を同期する必要があります。

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

双方向の関連付けの両端を同期することが重要である理由の詳細については、こちらの記事をご覧ください。


4

エンティティ定義では、に結合するための@JoinColumnを指定していません。次のようなものが必要になります。AccountTransaction

@Entity
public class Transaction {
    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    @JoinColumn(name = "accountId", referencedColumnName = "id")
    private Account fromAccount;
}

編集:まあ、私はあなたが@Tableあなたのクラスで注釈を使用しているなら役に立つと思います。へえ。:)


2
ええ、これはそうだとは思わない、すべて同じですが、@ JoinColumn(name = "fromAccount_id"、referencedColumnName = "id")を追加しましたが、機能しませんでした:)。
Paul Sanwald、

ええ、私は通常、エンティティをテーブルにマッピングするためにマッピングxmlファイルを使用しないので、通常はアノテーションベースであると想定しています。しかし、私が推測する必要がある場合は、hibernate.xmlを使用してエンティティをテーブルにマップしていますよね?
NemesisX00

いいえ、私はSpring Data JPAを使用しているので、すべてアノテーションベースです。反対側に「mappedBy」アノテーションがあります:@OneToMany(cascade = {CascadeType.ALL}、fetch = FetchType.EAGER、mappedBy = "fromAccount")
Paul Sanwald

4

1対多の関係を適切に管理するためにアノテーションが正しく宣言されている場合でも、この正確な例外が発生する可能性があります。Transactionアタッチされたデータモデルに新しい子オブジェクトを追加する場合、主キー値を管理する必要があります- 想定されていない場合を除きます。を呼び出す前に、次のように宣言された子エンティティの主キー値を指定するとpersist(T)、この例外が発生します。

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
....

この場合、注釈は、データベースが挿入時にエンティティの主キー値の生成を管理することを宣言しています。IDのセッターなどを使用して自分で提供すると、この例外が発生します。

または、実質的に同じですが、このアノテーション宣言は同じ例外になります。

@Entity
public class Transaction {
    @Id
    @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
    @GeneratedValue(generator="system-uuid")
    private Long id;
....

そのため、idすでに管理されているアプリケーションコードには値を設定しないでください。


4

何も解決equals()せず、この例外が引き続き発生する場合は、メソッドを確認してください。それに子コレクションを含めないでください。特に、埋め込まれたコレクションの構造が深い場合(たとえば、AにBが含まれている、BにCが含まれている、など)。

の例Account -> Transactions

  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

上記の例では、equals()チェックからトランザクションを削除します。これは、hibernateは古いオブジェクトを更新しようとしているのではなく、子コレクションの要素を変更するたびに永続化する新しいオブジェクトを渡すことを意味するためです。
もちろん、このソリューションはすべてのアプリケーションに適合するわけではありません。equalsおよびhashCodeメソッドあります。


1

多分それはOpenJPAのバグです。ロールバックすると@Versionフィールドがリセットされますが、pcVersionInitはtrueのままです。@Versionフィールドを宣言したAbstraceEntityがあります。pcVersionInitフィールドをリセットすることで回避できます。しかし、それは良い考えではありません。カスケード永続エンティティがある場合は機能しないと思います。

    private static Field PC_VERSION_INIT = null;
    static {
        try {
            PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
            PC_VERSION_INIT.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
        }
    }

    public T call(final EntityManager em) {
                if (PC_VERSION_INIT != null && isDetached(entity)) {
                    try {
                        PC_VERSION_INIT.set(entity, false);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                    }
                }
                em.persist(entity);
                return entity;
            }

            /**
             * @param entity
             * @param detached
             * @return
             */
            private boolean isDetached(final Object entity) {
                if (entity instanceof PersistenceCapable) {
                    PersistenceCapable pc = (PersistenceCapable) entity;
                    if (pc.pcIsDetached() == Boolean.TRUE) {
                        return true;
                    }
                }
                return false;
            }

1

すべてのアカウントにトランザクションを設定する必要があります。

foreach(Account account : accounts){
    account.setTransaction(transactionObj);
}

または、多くの側でIDをnullに設定するのに十分な場合(適切な場合)です。

// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());

foreach(Account account : accounts){
    account.setId(null);
}

transactionObj.setAccounts(accounts);

// just persist transactionObj using EntityManager merge() method.

1
cascadeType.MERGE,fetch= FetchType.LAZY

1
こんにちはJamesさん、ようこそ。コードのみの回答を試して回避する必要があります。これが質問に記載された問題をどのように解決するかを示してください(適用できるかどうか、APIレベルなど)。
Maarten Bodewes

VLQレビューア:meta.stackoverflow.com/questions/260411/…を参照してください。コードのみの回答は削除に値しません。適切なアクションは「正常に見える」を選択することです。
Nathan Hughes

1

私のSpring Data JPAベースの答え:私は単に @Transactional外側のメソッドにアノテーションをです。

なぜ機能するのか

アクティブなHibernateセッションコンテキストがないため、子エンティティはすぐに切り離されていました。Spring(Data JPA)トランザクションを提供すると、Hibernateセッションが確実に存在します。

参照:

https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/


0

私の場合、persistメソッドが使用されたときにトランザクションをコミットしていました。保存メソッドを保存に変更すると、解決されました。


0

上記のソリューションが一度だけ機能しない場合は、エンティティクラスのゲッターメソッドとセッターメソッドをコメント化し、idの値を設定しないでください(主キー)。これで機能します。


0

@OneToMany(mappedBy = "xxxx"、cascade = {CascadeType.MERGE、CascadeType.PERSIST、CascadeType.REMOVE})が私のために働いた。


0

次の前に依存オブジェクトを保存することで解決しました。

これは私がIDを設定していなかったために起こりました(自動生成されませんでした)。関係@ManytoOneで保存しようとしています

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