JPA: Cara memiliki relasi satu-ke-banyak dari jenis Entitas yang sama

99

Ada Kelas Entitas "A". Kelas A mungkin memiliki anak dengan tipe "A" yang sama. Juga "A" harus mengandung induknya jika itu adalah anak.

Apakah ini mungkin? Jika demikian, bagaimana cara memetakan relasi di kelas Entitas? ["A" memiliki kolom id.]

sanjayav
sumber

Jawaban:

170

Ya, ini mungkin. Ini adalah kasus khusus dari standar dua arah @ManyToOne/ @OneToManyhubungan. Itu istimewa karena entitas di setiap ujung hubungan adalah sama. Kasus umum dirincikan dalam Bagian 2.10.2 dari spesifikasi JPA 2.0 .

Inilah contoh yang berhasil. Pertama, kelas entitas 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...
}

Berikut adalah main()metode kasar yang mempertahankan tiga entitas tersebut:

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

Dalam kasus ini, ketiga contoh entitas harus dipertahankan sebelum transaksi dilakukan. Jika saya gagal mempertahankan salah satu entitas dalam grafik hubungan induk-anak, maka pengecualian akan diterapkan commit(). Di Eclipselink, ini adalah RollbackExceptiondetail ketidakkonsistenan.

Perilaku ini dapat dikonfigurasi melalui cascadeatribut pada A's @OneToManydan @ManyToOnepenjelasan. Misalnya, jika saya menyetel cascade=CascadeType.ALLkedua anotasi tersebut, saya dapat dengan aman mempertahankan salah satu entitas dan mengabaikan yang lainnya. Katakanlah saya bertahan parentdalam transaksi saya. Pelaksanaan traverse JPA parent's childrenproperti karena ditandai dengan CascadeType.ALL. Implementasi JPA menemukan sondan di daughtersana. Itu kemudian berlanjut pada kedua anak atas nama saya, meskipun saya tidak secara eksplisit memintanya.

Satu catatan lagi. Itu selalu menjadi tanggung jawab programmer untuk memperbarui kedua sisi hubungan dua arah. Dengan kata lain, setiap kali saya menambahkan anak ke beberapa orang tua, saya harus memperbarui properti induk anak tersebut. Memperbarui hanya satu sisi hubungan dua arah adalah kesalahan menurut JPA. Selalu perbarui kedua sisi hubungan. Ini ditulis dengan jelas pada halaman 42 dari spesifikasi JPA 2.0:

Perhatikan bahwa aplikasilah yang memikul tanggung jawab untuk menjaga konsistensi hubungan waktu proses — misalnya, untuk memastikan bahwa sisi "satu" dan "banyak" dari hubungan dua arah konsisten satu sama lain ketika aplikasi memperbarui hubungan pada waktu proses .

Dan LaRocque
sumber
Terima kasih banyak atas penjelasan detailnya! Contohnya adalah to the point dan berhasil di jalankan pertama.
sanjayav
@sunnyj Senang bisa membantu. Semoga berhasil dengan proyek Anda.
Dan LaRocque
Ini memenuhi masalah ini sebelumnya saat membuat entitas kategori yang memiliki sub kategori. Ini sangat membantu!
Truong Ha
@DanLaRocque mungkin saya salah paham (atau mengalami kesalahan pemetaan entitas), tetapi saya melihat perilaku yang tidak terduga. Saya memiliki hubungan satu-ke-banyak antara Pengguna dan Alamat. Ketika Pengguna yang ada menambahkan Alamat, saya mengikuti saran Anda untuk memperbarui Pengguna dan Alamat (dan memanggil 'simpan' pada keduanya). Tapi ini menghasilkan baris duplikat yang dimasukkan ke tabel Alamat saya. Apakah ini karena saya salah mengonfigurasi CascadeType saya di bidang alamat Pengguna?
Alex
@DanLaRocque apakah mungkin untuk mendefinisikan hubungan ini sebagai searah ??
Ali Arda Orhan
8

Bagi saya triknya adalah menggunakan hubungan banyak-ke-banyak. Misalkan entitas A Anda adalah divisi yang dapat memiliki sub-divisi. Kemudian (melewatkan detail yang tidak relevan):

@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;
  }
...
}

Karena saya memiliki logika bisnis yang luas di sekitar struktur hierarki dan JPA (berdasarkan model relasional) sangat lemah untuk mendukungnya, saya memperkenalkan antarmuka IHierarchyElementdan pendengar entitas 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);
        }
    }

}
topchef
sumber
1
Mengapa tidak menggunakan @OneToMany (mappedBy = "DIV_PARENT_ID") yang lebih sederhana daripada @ManyToMany (...) dengan atribut referensi mandiri? Mengetik ulang nama tabel dan kolom seperti itu melanggar KERING. Mungkin ada alasan untuk itu, tapi saya tidak melihatnya. Selain itu, contoh EntityListener rapi tetapi tidak portabel, dengan asumsi Topadalah hubungan. Halaman 93 dari spesifikasi JPA 2.0, Entity Listeners dan Metode Callback: "Secara umum, metode siklus hidup aplikasi portabel tidak boleh menjalankan operasi EntityManager atau Query, mengakses instance entitas lain, atau mengubah hubungan". Baik? Beri tahu saya jika saya tidak aktif.
Dan LaRocque
Solusi saya adalah 3 tahun menggunakan JPA 1.0. Saya mengadaptasinya tanpa diubah dari kode produksi. Saya yakin saya bisa mengambil beberapa nama kolom tapi bukan itu intinya. Jawaban Anda melakukannya dengan tepat dan lebih sederhana, tidak yakin mengapa saya menggunakan banyak ke banyak saat itu - tetapi berhasil dan saya yakin bahwa solusi yang lebih kompleks ada karena suatu alasan. Padahal, aku harus mengulanginya sekarang.
topchef
Ya, puncak adalah referensi diri, karenanya sebuah hubungan. Sebenarnya, saya tidak memodifikasinya - hanya menginisialisasi. Juga, itu searah sehingga tidak ada ketergantungan sebaliknya, itu tidak merujuk entitas lain selain diri. Sesuai kutipan Anda, spesifikasi memiliki "secara umum" yang berarti itu bukan definisi yang ketat. Saya percaya bahwa dalam kasus ini ada risiko portabilitas yang sangat rendah jika ada.
topchef