Bagaimana cara menghapus entitas dengan hubungan ManyToMany di JPA (dan baris tabel gabungan yang sesuai)?

91

Katakanlah saya memiliki dua entitas: Grup dan Pengguna. Setiap pengguna dapat menjadi anggota dari banyak grup dan setiap grup dapat memiliki banyak pengguna.

@Entity
public class User {
    @ManyToMany
    Set<Group> groups;
    //...
}

@Entity
public class Group {
    @ManyToMany(mappedBy="groups")
    Set<User> users;
    //...
}

Sekarang saya ingin menghapus grup (misalkan memiliki banyak anggota).

Masalahnya adalah ketika saya memanggil EntityManager.remove () di beberapa Grup, penyedia JPA (dalam kasus saya Hibernate) tidak menghapus baris dari tabel gabungan dan operasi penghapusan gagal karena kendala kunci asing. Memanggil remove () pada Pengguna berfungsi dengan baik (saya kira ini ada hubungannya dengan memiliki sisi hubungan).

Jadi bagaimana cara menghapus grup dalam kasus ini?

Satu-satunya cara yang bisa saya lakukan adalah memuat semua pengguna di grup, lalu untuk setiap pengguna, hapus grup saat ini dari grupnya dan perbarui pengguna. Tetapi tampaknya konyol bagi saya untuk memanggil update () pada setiap pengguna dari grup hanya untuk dapat menghapus grup ini.

rdk
sumber

Jawaban:

86
  • Kepemilikan relasi ditentukan oleh tempat Anda menempatkan atribut 'mappedBy' ke anotasi. Entitas yang Anda masukkan 'mappedBy' adalah entitas yang BUKAN pemiliknya. Tidak ada kesempatan bagi kedua belah pihak untuk menjadi pemilik. Jika Anda tidak memiliki kasus penggunaan 'hapus pengguna', Anda dapat dengan mudah memindahkan kepemilikan ke Groupentitas, karena saat ini Useradalah pemiliknya.
  • Di sisi lain, Anda belum menanyakannya, tetapi satu hal yang perlu diketahui. The groupsdan userstidak dikombinasikan satu sama lain. Maksud saya, setelah menghapus instance User1 dari Group1.users, koleksi User1.groups tidak berubah secara otomatis (yang cukup mengejutkan bagi saya),
  • Secara keseluruhan, saya sarankan Anda memutuskan siapa pemiliknya. Katakanlah Useradalah pemiliknya. Kemudian saat menghapus pengguna, relasi kelompok pengguna akan diperbarui secara otomatis. Tetapi saat menghapus grup, Anda harus berhati-hati saat menghapus relasi sendiri seperti ini:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Grzegorz Oledzki
sumber
Thnx! Saya memiliki masalah yang sama dan solusi Anda menyelesaikannya. Tetapi saya harus tahu apakah ada cara lain untuk menyelesaikan masalah ini. Ini menghasilkan kode yang mengerikan. Mengapa tidak bisa em.remove (entitas) dan hanya itu?
Royi Freifeld
2
Apakah ini dioptimalkan di balik layar? karena saya tidak ingin menanyakan seluruh dataset.
Menyerahkan
1
Ini benar-benar pendekatan yang buruk, bagaimana jika Anda memiliki beberapa ribu pengguna di grup itu?
Sergiy Sokolenko
2
Ini tidak memperbarui tabel relasi, dalam tabel relasi baris tentang relasi ini masih ada. Saya tidak menguji tetapi Itu berpotensi menyebabkan masalah.
Saygın Doğu
@SergiySokolenko tidak ada kueri database yang dijalankan di loop for. Semuanya dikumpulkan setelah fungsi Transaksional dibiarkan.
Kilves
42

Berikut ini bekerja untuk saya. Tambahkan metode berikut ke entitas yang bukan pemilik hubungan (Grup)

@PreRemove
private void removeGroupsFromUsers() {
    for (User u : users) {
        u.getGroups().remove(this);
    }
}

Perlu diingat bahwa agar ini berfungsi, Grup harus memiliki daftar Pengguna yang diperbarui (yang tidak dilakukan secara otomatis). jadi setiap kali Anda menambahkan Grup ke daftar grup di entitas Pengguna, Anda juga harus menambahkan Pengguna ke daftar pengguna di entitas Grup.

damian
sumber
1
cascade = CascadeType.ALL tidak boleh disetel saat Anda ingin menggunakan solusi ini. Kalau tidak, itu bekerja dengan sempurna!
ltsstar
@ Damian Hai saya telah menggunakan solusi Anda, tetapi saya mendapat kesalahan concurrentModficationException saya pikir seperti yang Anda tunjukkan alasannya mungkin Grup harus memiliki daftar Pengguna yang diperbarui. Karena jika saya menambahkan lebih dari satu grup ke pengguna, saya mendapatkan pengecualian ini ...
Adnan Abdul Khaliq
@Damian Perlu diingat bahwa agar ini berfungsi, Grup harus memiliki daftar Pengguna yang diperbarui (yang tidak dilakukan secara otomatis). jadi setiap kali Anda menambahkan Grup ke daftar grup di entitas Pengguna, Anda juga harus menambahkan Pengguna ke daftar pengguna di entitas Grup. Bisakah Anda menjelaskan hal ini, beri saya contoh ,, terima kasih
Adnan Abdul Khaliq
27

Saya menemukan solusi yang mungkin, tetapi ... Saya tidak tahu apakah itu solusi yang baik.

@Entity
public class Role extends Identifiable {

    @ManyToMany(cascade ={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name="Role_Permission",
            joinColumns=@JoinColumn(name="Role_id"),
            inverseJoinColumns=@JoinColumn(name="Permission_id")
        )
    public List<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(List<Permission> permissions) {
        this.permissions = permissions;
    }
}

@Entity
public class Permission extends Identifiable {

    @ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name="Role_Permission",
            joinColumns=@JoinColumn(name="Permission_id"),
            inverseJoinColumns=@JoinColumn(name="Role_id")
        )
    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

Saya telah mencoba ini dan berhasil. Saat Anda menghapus Peran, relasi juga akan dihapus (tetapi bukan entitas Izin) dan saat Anda menghapus Izin, relasi dengan Peran juga dihapus (tetapi bukan instance Peran). Tapi kami memetakan relasi searah dua kali dan kedua entitas adalah pemilik relasi. Mungkinkah ini menyebabkan masalah pada mode Hibernasi? Jenis masalah apa?

Terima kasih!

Kode di atas berasal dari postingan lain yang terkait.

jelies
sumber
masalah penyegaran koleksi?
jelies
12
Ini tidak benar. Bidirectional ManyToMany harus memiliki satu dan hanya satu sisi pemilik hubungan. Solusi ini menjadikan kedua belah pihak sebagai pemilik dan pada akhirnya akan menghasilkan duplikat record. Untuk menghindari rekaman duplikat, Kumpulan harus digunakan sebagai ganti Daftar. Namun, menggunakan Set hanyalah solusi untuk melakukan sesuatu yang tidak disarankan.
L.Holanda
4
lalu apa praktik terbaik untuk skenario umum seperti itu?
SalutonMondo
8

Sebagai alternatif untuk solusi JPA / Hibernate: Anda dapat menggunakan klausa CASCADE DELETE dalam definisi database dari kunci foregin Anda di tabel gabungan, seperti (sintaks Oracle):

CONSTRAINT fk_to_group
     FOREIGN KEY (group_id)
     REFERENCES group (id)
     ON DELETE CASCADE

Dengan cara itu DBMS sendiri secara otomatis menghapus baris yang mengarah ke grup saat Anda menghapus grup. Dan itu berfungsi apakah penghapusan dibuat dari Hibernate / JPA, JDBC, secara manual di DB atau dengan cara lain.

fitur penghapusan kaskade didukung oleh semua DBMS utama (Oracle, MySQL, SQL Server, PostgreSQL).

Pierre Henry
sumber
3

Untuk apa nilainya, saya menggunakan EclipseLink 2.3.2.v20111125-r10461 dan jika saya memiliki hubungan searah @ManyToMany, saya mengamati masalah yang Anda gambarkan. Namun, jika saya mengubahnya menjadi hubungan dua arah @ManyToMany, saya dapat menghapus entitas dari sisi non-pemilik dan tabel JOIN diperbarui dengan tepat. Ini semua tanpa menggunakan atribut kaskade apa pun.

NBW
sumber
2

Ini bekerja untuk saya:

@Transactional
public void remove(Integer groupId) {
    Group group = groupRepository.findOne(groupId);
    group.getUsers().removeAll(group.getUsers());

    // Other business logic

    groupRepository.delete(group);
}

Juga, tandai metode @Transactional (org.springframework.transaction.annotation.Transactional), ini akan melakukan seluruh proses dalam satu sesi, menghemat waktu.

Mehul Katpara
sumber
1

Ini adalah solusi yang bagus. Bagian terbaiknya ada di sisi SQL - menyempurnakan ke level mana pun itu mudah.

Saya menggunakan MySql dan MySql Workbench ke Cascade saat menghapus untuk KUNCI Asing yang Diperlukan.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
pengguna1776955
sumber
Selamat datang di SO. Harap luangkan waktu sejenak dan lihat ini untuk meningkatkan pemformatan dan pembacaan bukti Anda: stackoverflow.com/help/how-to-ask
petezurich
1

Inilah yang akhirnya saya lakukan. Semoga seseorang dapat merasakan manfaatnya.

@Transactional
public void deleteGroup(Long groupId) {
    Group group = groupRepository.findById(groupId).orElseThrow();
    group.getUsers().forEach(u -> u.getGroups().remove(group));
    userRepository.saveAll(group.getUsers());
    groupRepository.delete(group);
}
Stephen Paul
sumber
0

Untuk kasus saya, saya menghapus mappedBy dan menggabungkan tabel seperti ini:

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "user_group", joinColumns = {
        @JoinColumn(name = "user", referencedColumnName = "user_id")
}, inverseJoinColumns = {
        @JoinColumn(name = "group", referencedColumnName = "group_id")
})
private List<User> users;

@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JsonIgnore
private List<Group> groups;
szachMati
sumber
3
Jangan gunakan cascadeType.all dalam many-to-many. Saat dihapus, ini menyebabkan data A menghapus semua data B yang terkait. Dan data B menghapus semua data A terkait. Dan seterusnya. Tidak-tidak.
Yosia
0

Ini berfungsi untuk saya pada masalah serupa di mana saya gagal menghapus pengguna karena referensi. Terima kasih

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Me Me
sumber