Як видалити сутність із відносинами ManyToMany у JPA (та відповідні рядки таблиці об’єднання)?


91

Скажімо, у мене є дві сутності: Група та Користувач. Кожен користувач може бути членом багатьох груп, і кожна група може мати багато користувачів.

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

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

Тепер я хочу видалити групу (припустимо, у ній багато учасників).

Проблема полягає в тому, що коли я викликаю EntityManager.remove () у якійсь групі, постачальник JPA (у моєму випадку Hibernate) не видаляє рядки з таблиці об’єднань, а операція видалення не вдається через обмеження зовнішнього ключа. Виклик remove () для Користувача працює нормально (мабуть, це пов’язано з власницькою стороною відносин).

То як я можу видалити групу в цьому випадку?

Єдиний спосіб, який я міг би придумати, - це завантажити всіх користувачів у групі, а потім для кожного користувача видалити поточну групу зі своїх груп та оновити користувача. Але мені здається смішним викликати update () для кожного користувача з групи, лише щоб мати можливість видалити цю групу.

Відповіді:


86
  • Право власності на відношення визначається тим, де ви розміщуєте атрибут 'mappedBy' до анотації. Сутність, яку ви поставили 'mappedBy', є тією, яка НЕ ​​є власником. Немає шансів для обох сторін бути власниками. Якщо у вас немає випадку використання "видалити користувача", ви можете просто перенести право власності на Groupсутність, оскільки зараз Userвласник є власником.
  • З іншого боку, ви не запитували про це, але одне варто знати. groupsІ usersНЕ поєднуються один з одним. Я маю на увазі, що після видалення екземпляра User1 з Group1.users колекції User1.groups не змінюються автоматично (що для мене досить дивно),
  • Загалом, я б запропонував вам вирішити, хто є власником. Скажімо, Userце власник. Тоді при видаленні користувача відношення user-group буде оновлено автоматично. Але при видаленні групи потрібно подбати про те, щоб видалити відношення самостійно так:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()

Thnx! У мене була та ж проблема, і ваше рішення її вирішило. Але я повинен знати, чи є інший спосіб вирішити цю проблему. Він видає жахливий код. Чому це не може бути em.remove (сутність) і все?
Рой Фрейфельд,

2
Це оптимізовано за лаштунками? Тому що я не хочу запитувати весь набір даних.
Ced

1
Це дійсно поганий підхід. Що робити, якщо у вас є кілька тисяч користувачів у цій групі?
Сергій Соколенко

2
Це не оновлює таблицю відношень, у таблиці відносин рядки про це відношення все ще залишаються. Я не тестував, але це потенційно може спричинити проблеми.
Saygın Do 11u

@SergiySokolenko в циклі for не виконуються запити до бази даних. Усі вони групуються після того, як функція транзакції залишається.
Кілвес,

42

Наступне працює для мене. Додайте наступний метод до сутності, яка не є власником відносин (Група)

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

Майте на увазі, що для того, щоб це працювало, Група повинна мати оновлений список Користувачів (що не робиться автоматично). тому кожного разу, коли ви додаєте групу до списку груп в сутності User, ви також повинні додати користувача до списку користувачів в сутності Group.


1
cascade = CascadeType.ALL не слід встановлювати, коли ви хочете використовувати це рішення. В іншому випадку це працює ідеально!
ltsstar

@damian Привіт, я скористався вашим рішенням, але у мене виникла помилка concurrentModficationException. Я думаю, як ви вказали, причиною може бути те, що група повинна мати оновлений список користувачів. Тому що якщо я додаю більше однієї групи до користувача, я отримую цей виняток ...
Аднан Абдул Халік

@Damian Майте на увазі, що, щоб це працювало, Група повинна мати оновлений список Користувачів (що не робиться автоматично). тому кожного разу, коли ви додаєте групу до списку груп в сутності User, ви також повинні додати користувача до списку користувачів в сутності Group. Не могли б ви детальніше це
Аднан Абдул

27

Я знайшов можливе рішення, але ... Я не знаю, чи це хороше рішення.

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

Я спробував це, і це працює. При видаленні Ролі також видаляються відносини (але не сутності Дозволу), а коли Ви видаляєте Дозвіл, відношення з Роллю також видаляються (але не екземпляр Ролі). Але ми відображаємо односпрямоване відношення два рази, і обидві сутності є власником відношення. Чи може це спричинити деякі проблеми зі сплячим режимом? Який тип проблем?

Дякую!

Код вище з іншої публікації, пов’язаної.


проблеми з оновленням колекції?
желе

12
Це неправильно. Двонаправлені ManyToMany повинні мати одну і тільки одну сторону власника відносин. Це рішення робить обидві сторони власниками і врешті-решт призведе до дублікатів записів. Щоб уникнути дублювання записів, замість Списків слід використовувати набори. Але використання Set - це лише обхідний шлях для того, щоб зробити те, що не рекомендується.
L. Holanda

4
то яка найкраща практика для такого загального сценоріо?
SalutonMondo

8

Як альтернативу рішенням JPA / Hibernate: ви можете використовувати пункт CASCADE DELETE у визначенні бази даних вашого ключа foregin у вашій таблиці об’єднання, наприклад (синтаксис Oracle):

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

Таким чином сама СУБД автоматично видаляє рядок, який вказує на групу, коли ви видаляєте групу. І це працює, незалежно від того, чи видаляється з Hibernate / JPA, JDBC, вручну в БД або будь-яким іншим способом.

функція каскадного видалення підтримується усіма основними СУБД (Oracle, MySQL, SQL Server, PostgreSQL).


3

Для чого я вартую, я використовую EclipseLink 2.3.2.v20111125-r10461, і якщо у мене є односпрямоване відношення @ManyToMany, я спостерігаю проблему, яку ви описуєте. Однак, якщо я зміню його на двонаправлене відношення @ManyToMany, я зможу видалити сутність з боку, що не є власником, і таблиця JOIN оновлюється належним чином. Це все без використання будь-яких атрибутів каскаду.


2

Це працює для мене:

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

    // Other business logic

    groupRepository.delete(group);
}

Також позначте метод @Transactional (org.springframework.transaction.annotation.Transactional), це буде робити весь процес за один сеанс, економить час.


1

Це хороше рішення. Найкраще - на стороні SQL - тонке налаштування на будь-який рівень легко.

Я використовував MySql та MySql Workbench для каскадного видалення для необхідного іноземного КЛЮЧУ.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;

Ласкаво просимо до SO. Будь ласка , знайдіть час і подивіться на це , щоб поліпшити ваше форматування і коректури: stackoverflow.com/help/how-to-ask
petezurich

1

Це те, що я закінчив робити. Сподіваємось, комусь це може виявитися корисним.

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

0

Для мого випадку я видалив mappedBy і приєднався до таблиць, як це:

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

3
Не використовуйте cascadeType.all у багато-до-багатьох. При видаленні змушує запис A видаляти всі пов'язані записи B. А записи B видаляють усі пов’язані записи A. І так далі. Велике ні-ні.
Йосія

0

Це працює для мене з подібною проблемою, коли мені не вдалося видалити користувача через посилання. Дякую

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.