JPA:同じエンティティタイプの1対多の関係を作成する方法


99

エンティティクラス「A」があります。クラスAには、同じタイプ「A」の子がある場合があります。また、「A」は、子の場合は親を保持する必要があります。

これは可能ですか?もしそうなら、どのようにエンティティクラスの関係をマッピングする必要がありますか?["A"にはid列があります。]

回答:


170

はい、可能です。これは、標準の双方向@ManyToOne/ @OneToMany関係の特殊なケースです。関係の両端のエンティティは同じであるため、これは特別です。一般的なケースの詳細は、JPA 2.0仕様のセクション2.10.2に記載されています。

これが実際の例です。まず、エンティティクラスA

@Entity
public class A implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    @ManyToOne
    private A parent;
    @OneToMany(mappedBy="parent")
    private Collection<A> children;

    // Getters, Setters, serialVersionUID, etc...
}

このmain()ようなエンティティを3つ保持する大まかな方法を次に示します。

public static void main(String[] args) {

    EntityManager em = ... // from EntityManagerFactory, injection, etc.

    em.getTransaction().begin();

    A parent   = new A();
    A son      = new A();
    A daughter = new A();

    son.setParent(parent);
    daughter.setParent(parent);
    parent.setChildren(Arrays.asList(son, daughter));

    em.persist(parent);
    em.persist(son);
    em.persist(daughter);

    em.getTransaction().commit();
}

この場合、3つのエンティティインスタンスはすべて、トランザクションのコミット前に永続化する必要があります。親子関係のグラフのエンティティの1つを永続化できない場合、例外がスローされcommit()ます。Eclipselinkでは、これはRollbackException不整合の詳細です。

この動作はを通じて設定可能であるcascade上の属性A@OneToMany@ManyToOne注釈。たとえばcascade=CascadeType.ALL、これらの注釈の両方を設定した場合、エンティティの1つを安全に永続化し、他のエンティティを無視できます。私parentが私のトランザクションに固執したとしましょう。JPA実装はでマークされているため、parentchildrenプロパティを走査しCascadeType.ALLます。JPA実装発見sondaughterそこに。その後、私が明示的に要求しなかったとしても、それは私の代わりに両方の子を永続化します。

もう1つ注意してください。双方向の関係の両側を更新することは、常にプログラマの責任です。言い換えると、ある親に子を追加するたびに、それに応じて子の親プロパティを更新する必要があります。双方向関係の片側のみを更新すると、JPAではエラーになります。関係の両側を常に更新します。これは、JPA 2.0仕様の42ページに明確に記述されています。

ランタイムの関係の一貫性を維持する責任があるのはアプリケーションです。たとえば、アプリケーションが実行時に関係を更新したときに双方向の関係の「片側」と「多数」の側が互いに一貫していることを保証します。 。


詳しい説明ありがとうございます!例は要点であり、最初の実行で機能しました。
sanjayav 2010

@sunnyj助けてくれてうれしい。プロジェクトで頑張ってください。
Dan LaRocque、2010

以前は、サブカテゴリを持つカテゴリエンティティを作成するときにこの問題に遭遇しました。参考になりました!
Truong Ha

@DanLaRocqueおそらく誤解している(またはエンティティマッピングエラーがある)が、予期しない動作が発生している。ユーザーとアドレスの間に1対多の関係があります。既存のユーザーがアドレスを追加するとき、私はあなたの提案に従ってユーザーとアドレスの両方を更新しました(そして両方で 'save'を呼び出しました)。しかし、これにより、重複した行がアドレステーブルに挿入されました。これは、ユーザーのアドレスフィールドでCascadeTypeを誤って構成したためですか?
アレックス

@DanLaRocqueこの関係を一方向として定義することは可能ですか?
Ali Arda Orhan、2015

8

私にとってのコツは、多対多の関係を使用することでした。エンティティAがサブディビジョンを持つことができるディビジョンであるとします。次に(無関係な詳細をスキップ):

@Entity
@Table(name = "DIVISION")
@EntityListeners( { HierarchyListener.class })
public class Division implements IHierarchyElement {

  private Long id;

  @Id
  @Column(name = "DIV_ID")
  public Long getId() {
        return id;
  }
  ...
  private Division parent;
  private List<Division> subDivisions = new ArrayList<Division>();
  ...
  @ManyToOne
  @JoinColumn(name = "DIV_PARENT_ID")
  public Division getParent() {
        return parent;
  }

  @ManyToMany
  @JoinTable(name = "DIVISION", joinColumns = { @JoinColumn(name = "DIV_PARENT_ID") }, inverseJoinColumns = { @JoinColumn(name = "DIV_ID") })
  public List<Division> getSubDivisions() {
        return subDivisions;
  }
...
}

階層構造の周りにいくつかの広範なビジネスロジックがあり、JPA(リレーショナルモデルに基づく)はそれをサポートするには非常に弱いため、インターフェースIHierarchyElementとエンティティリスナーを導入しましたHierarchyListener

public interface IHierarchyElement {

    public String getNodeId();

    public IHierarchyElement getParent();

    public Short getLevel();

    public void setLevel(Short level);

    public IHierarchyElement getTop();

    public void setTop(IHierarchyElement top);

    public String getTreePath();

    public void setTreePath(String theTreePath);
}


public class HierarchyListener {

    @PrePersist
    @PreUpdate
    public void setHierarchyAttributes(IHierarchyElement entity) {
        final IHierarchyElement parent = entity.getParent();

        // set level
        if (parent == null) {
            entity.setLevel((short) 0);
        } else {
            if (parent.getLevel() == null) {
                throw new PersistenceException("Parent entity must have level defined");
            }
            if (parent.getLevel() == Short.MAX_VALUE) {
                throw new PersistenceException("Maximum number of hierarchy levels reached - please restrict use of parent/level relationship for "
                        + entity.getClass());
            }
            entity.setLevel(Short.valueOf((short) (parent.getLevel().intValue() + 1)));
        }

        // set top
        if (parent == null) {
            entity.setTop(entity);
        } else {
            if (parent.getTop() == null) {
                throw new PersistenceException("Parent entity must have top defined");
            }
            entity.setTop(parent.getTop());
        }

        // set tree path
        try {
            if (parent != null) {
                String parentTreePath = StringUtils.isNotBlank(parent.getTreePath()) ? parent.getTreePath() : "";
                entity.setTreePath(parentTreePath + parent.getNodeId() + ".");
            } else {
                entity.setTreePath(null);
            }
        } catch (UnsupportedOperationException uoe) {
            LOGGER.warn(uoe);
        }
    }

}

1
自己参照属性を持つ@ManyToMany(...)の代わりに、より単純な@OneToMany(mappedBy = "DIV_PARENT_ID")を使用しないのはなぜですか?このようにテーブルと列の名前を再入力すると、DRYに違反します。理由はあるかもしれませんが、私にはわかりません。また、EntityListenerの例はきちんとしていますが、ポータブルでTopはありません。JPA 2.0仕様のページ93、エンティティリスナーとコールバックメソッド:「一般的に、ポータブルアプリケーションのライフサイクルメソッドは、EntityManagerまたはQueryオペレーションを呼び出したり、他のエンティティインスタンスにアクセスしたり、関係を変更したりするべきではありません。」正しい?休みかどうか教えてください。
Dan LaRocque、2010

私のソリューションは、JPA 1.0を使用して3歳です。私はそれを製品コードから変更せずに適合させました。私はいくつかの列名を取り出すことができると確信していますが、それは重要ではありませんでした。あなたの答えはそれを正確かつ単純にしますが、なぜ私が多対多を使用したのかはわかりませんが、それはうまくいき、理由のためにもっと複雑な解決策があったと確信しています。でも、今これをもう一度見直す必要があります。
topchef 2010

はい、トップは自己参照であるため、関係があります。厳密に言えば、変更はしません。初期化するだけです。また、一方向なので、逆に依存関係はありません。自己以外の他のエンティティを参照しません。あなたの引用ごとの仕様には「一般」があり、厳密な定義ではありません。この場合、移植性のリスクは非常に低いと思います。
topchef 2010
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.