Como remover entidade com relacionamento ManyToMany em JPA (e linhas de tabela de junção correspondentes)?

92

Digamos que eu tenha duas entidades: Grupo e Usuário. Cada usuário pode ser membro de muitos grupos e cada grupo pode ter muitos usuários.

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

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

Agora, quero remover um grupo (digamos que ele tenha muitos membros).

O problema é que quando eu chamo EntityManager.remove () em algum Grupo, o provedor JPA (no meu caso Hibernate) não remove linhas da tabela de junção e a operação de exclusão falha devido a restrições de chave estrangeira. Chamar remove () em User funciona bem (acho que isso tem algo a ver com o lado proprietário do relacionamento).

Então, como posso remover um grupo neste caso?

A única maneira que eu poderia inventar é carregar todos os usuários no grupo, então para cada usuário remover o grupo atual de seus grupos e atualizar o usuário. Mas me parece ridículo chamar update () em cada usuário do grupo apenas para poder deletar este grupo.

rdk
fonte

Respostas:

87
  • A propriedade da relação é determinada por onde você coloca o atributo 'mappedBy' na anotação. A entidade que você colocou 'mappedBy' é aquela que NÃO é o proprietário. Não há chance de ambos os lados serem proprietários. Se você não tiver um caso de uso 'excluir usuário', poderá simplesmente mover a propriedade para a Groupentidade, já que atualmente Useré o proprietário.
  • Por outro lado, você não tem perguntado sobre isso, mas uma coisa que vale a pena saber. Os groupse usersnão estão combinados um com o outro. Quero dizer, depois de excluir a instância User1 de Group1.users, as coleções de User1.groups não são alteradas automaticamente (o que é bastante surpreendente para mim),
  • Em suma, sugiro que você decida quem é o proprietário. Digamos que Useré o proprietário. Então, ao excluir um usuário, a relação usuário-grupo será atualizada automaticamente. Mas ao deletar um grupo, você deve cuidar de deletar a relação você mesmo assim:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Grzegorz Oledzki
fonte
Thnx! Eu tive o mesmo problema e sua solução resolveu. Mas preciso saber se existe outra maneira de resolver esse problema. Ele produz um código horrível. Por que não pode ser em.remove (entidade) e pronto ?
Royi Freifeld
2
Isso é otimizado nos bastidores? porque eu não quero consultar o conjunto de dados inteiro.
Ced
1
Esta é uma abordagem realmente ruim, e se você tiver vários milhares de usuários nesse grupo?
Sergiy Sokolenko de
2
Isso não atualiza a tabela de relação, na tabela de relação as linhas sobre esta relação ainda permanecem. Eu não testei, mas isso poderia causar problemas.
Saygın Doğu
@SergiySokolenko nenhuma consulta ao banco de dados é executada nesse loop for. Todos eles são agrupados depois que a função Transacional é deixada.
Kilves
43

O seguinte funciona para mim. Adicione o seguinte método à entidade que não é a proprietária do relacionamento (Grupo)

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

Lembre-se que para que isso funcione, o Grupo deve ter uma lista de Usuários atualizada (o que não é feito automaticamente). portanto, toda vez que você adiciona um Grupo à lista de grupos na entidade Usuário, também deve adicionar um Usuário à lista de usuários na entidade Grupo.

damian
fonte
1
cascade = CascadeType.ALL não deve ser definido quando você deseja usar esta solução. Caso contrário, funciona perfeitamente!
ltsstar de
@damian Olá, usei sua solução, mas recebi um erro concurrentModficationException, acho que, como você apontou, o motivo pode ser o Grupo deve ter uma lista de usuários atualizada. Porque se eu adicionar mais de um grupo ao usuário, obtenho essa exceção ...
Adnan Abdul Khaliq
@Damian Lembre-se que para que isso funcione, o Grupo deve ter uma lista de Usuários atualizada (o que não é feito automaticamente). portanto, toda vez que você adiciona um Grupo à lista de grupos na entidade Usuário, também deve adicionar um Usuário à lista de usuários na entidade Grupo. Você poderia elaborar sobre isso, me dê um exemplo ,, thnks
Adnan Abdul Khaliq
27

Encontrei uma solução possível, mas ... não sei se é uma boa solução.

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

Eu tentei isso e funciona. Quando você exclui Role, também as relações são excluídas (mas não as entidades Permission) e quando você exclui Permission, as relações com Role são excluídas também (mas não a instância Role). Mas estamos mapeando uma relação unidirecional duas vezes e ambas as entidades são as proprietárias da relação. Isso poderia causar alguns problemas para o Hibernate? Que tipo de problemas?

Obrigado!

O código acima é de outro post relacionado.

geléias
fonte
problemas de atualização da coleção?
jelies
12
Isso não está correto. ManyToMany bidirecional deve ter um e apenas um lado proprietário do relacionamento. Essa solução está tornando ambos os lados proprietários e, eventualmente, resultará em registros duplicados. Para evitar registros duplicados, Conjuntos devem ser usados ​​em vez de Listas. Mas, usar Set é apenas uma solução alternativa para fazer algo que não é recomendado.
L. Holanda
4
então qual é a melhor prática para um cenário tão comum?
SalutonMondo
8

Como alternativa às soluções JPA / Hibernate: você pode usar uma cláusula CASCADE DELETE na definição do banco de dados de sua chave anterior em sua tabela de junção, como (sintaxe Oracle):

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

Dessa forma, o próprio DBMS exclui automaticamente a linha que aponta para o grupo quando você exclui o grupo. E funciona seja a exclusão feita do Hibernate / JPA, JDBC, manualmente no BD ou de qualquer outra forma.

o recurso de exclusão em cascata é compatível com todos os SGBD principais (Oracle, MySQL, SQL Server, PostgreSQL).

Pierre Henry
fonte
3

Para constar, estou usando EclipseLink 2.3.2.v20111125-r10461 e se eu tiver um relacionamento unidirecional @ManyToMany, observo o problema que você descreve. No entanto, se eu alterar para um relacionamento @ManyToMany bidirecional, poderei excluir uma entidade do lado que não é proprietário e a tabela JOIN será atualizada de forma adequada. Isso tudo sem o uso de atributos em cascata.

NBW
fonte
2

Isso funciona para mim:

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

    // Other business logic

    groupRepository.delete(group);
}

Além disso, marque o método @Transactional (org.springframework.transaction.annotation.Transactional), isso fará todo o processo em uma sessão, economiza tempo.

Mehul Katpara
fonte
1

Esta é uma boa solução. A melhor parte está no lado do SQL - o ajuste fino em qualquer nível é fácil.

Usei MySql e MySql Workbench para cascatear na exclusão da chave estrangeira necessária.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
user1776955
fonte
Bem-vindo ao SO. Por favor, reserve um momento e analise isso para melhorar sua formatação e leitura de
revisão
0

No meu caso, excluí mappedBy e juntei tabelas como esta:

@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
fonte
3
Não use cascadeType.all em muitos para muitos. Ao excluir, faz com que o registro A exclua todos os registros B associados. E os registros B excluem todos os registros A associados. E assim por diante. Grande não, não.
Josiah
0

Isso funciona para mim em um problema semelhante, em que não consegui excluir o usuário devido à referência. Obrigado

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Eu eu
fonte
0

Isso é o que acabei fazendo. Esperançosamente, alguém pode achar isso útil.

@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
fonte
Se um grupo tiver 100 usuários, deleteGroupirá acionar 1 (selecionar) + 100 (excluir) + 100 (atualizar) + 1 (excluir) consultas.
Henri