web-dev-qa-db-ja.com

JPA/Hibernate:永続化に渡された分離エンティティ

多対1の関係を含むJPA永続オブジェクトモデルがあります。アカウントには多数のトランザクションがあります。トランザクションにはアカウントが1つあります。

これがコードの一部です。

@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オブジェクトを正しく永続化することができます。しかし、トランザクションを作成すると、 既存の既存のアカウント を使用し、 トランザクション を永続化すると、例外が発生します。

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.paulsanwald.Account
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.Java:141) 

それで、私は取引を含む口座を永続させることができますが、口座を持つ取引は永続させることができません。これは、アカウントが添付されていない可能性があるためだと思いましたが、このコードでも同じ例外が発生します。

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

すでに永続化されているAccountオブジェクトに関連付けられているトランザクションを正しく保存する方法を教えてください。

179
Paul Sanwald

これは典型的な双方向の一貫性の問題です。 このリンク および このリンクで詳しく説明されています。

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

メニーサイドの設定例は このリンクにあります。

セッターを修正したら、Entityアクセスタイプを "Property"に宣言します。 "Property"アクセスタイプを宣言するベストプラクティスは、すべてのアノテーションをメンバプロパティから対応するゲッターに移動することです。大きな注意点は、エンティティクラス内で "Field"アクセスタイプと "Property"アクセスタイプを混在させることではありません。それ以外の場合、動作はJSR-317仕様で定義されていません。

109
Sym-Sym

解決策は簡単です。単にCascadeType.MERGECascadeType.PERSISTの代わりにCascadeType.ALLを使うだけです。

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

あなたが選別されていることを願っています。

210
ochiWlad

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

ここでは完全なコードを表示していないため、トランザクションパターンを再確認することはできません。このような状況に到達する1つの方法は、マージおよび永続化を実行するときにアクティブなトランザクションがない場合です。その場合、永続性プロバイダーは、実行するすべてのJPA操作に対して新しいトランザクションを開き、呼び出しが戻る前にすぐにコミットして閉じることが期待されます。この場合、最初のトランザクションでマージが実行され、マージメソッドが戻った後、トランザクションが完了して閉じられ、返されたエンティティが切り離されます。その下に永続化すると、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);
}
14
Zds

おそらくこの場合、あなたはaccountオブジェクトをマージロジックを使って取得し、そしてpersistは新しいオブジェクトを永続化するために使われ、階層が既に永続化されたオブジェクトを持っているならそれは文句を言うでしょう。そのような場合は、saveOrUpdateではなくpersistを使用してください。

9
dan

Persistメソッドにid(pk)を渡さないでください。あるいはpersist()の代わりにsave()メソッドを試してください。

8
Angad Bansode

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

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

FetchType.EAGER@ManyToOneのデフォルトであると同時に削除することもできます)

それで全部です!

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

ただし、一時的な(新しい)エンティティだけがpersist(この場合はTransaction)に渡されます。切り離された(または他の一時的でない状態の)ものはそうではないかもしれません(この場合はAccount、すでにDBにあるため)。

したがって、例外 "永続化に渡された分離エンティティ" が発生します。 Accountエンティティが意味されています! Transactionを呼び出したpersistではありません。


あなたは一般的に子供から親に伝播したくありません。残念なことに、本には(たとえ良いものでも)、ネットを通じてたくさんのコード例があります。よくわからない、なぜか……何も考えずに何度も何度もコピーしているのかもしれません….

その@ManyToOneにremove(transaction)を呼び出しても "cascade ALL"がまだ残っているとどうなりますか。 account(btw、他のすべてのトランザクションと一緒に!)も同様にDBから削除されます。しかし、それはあなたの意図ではありませんでしたね。

4
Eugen Labun

何も役に立たず、まだこの例外を受けているのであれば、equals()メソッドを見直してください - そして子コレクションを含めないでください。特にあなたが埋め込みコレクションの深い構造を持っているなら(例えば、AはBsを含み、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メソッドに含めるものを慎重に設計する必要があります。

4
FazoM

エンティティ定義で、Accountに結合されたTransaction@JoinColumn を指定していません。あなたはこのようなものが欲しいでしょう:

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

編集:まあ、私はあなたがあなたのクラスに@Tableアノテーションを使用していた場合に便利だろうと思います。ほら:)

4
NemesisX00

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の値を設定しないでください。

2
dan

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

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
stakahop

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
Yaocl

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

0
Shubham
cascadeType.MERGE,fetch= FetchType.LAZY
0
James Siva

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

0
H.Ostwal