¿Cómo eliminar la entidad con la relación ManyToMany en JPA (y las filas de la tabla de unión correspondientes)?

91

Digamos que tengo dos entidades: Grupo y Usuario. Cada usuario puede ser miembro de muchos grupos y cada grupo puede tener muchos usuarios.

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

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

Ahora quiero eliminar un grupo (digamos que tiene muchos miembros).

El problema es que cuando llamo a EntityManager.remove () en algún grupo, el proveedor de JPA (en mi caso, Hibernate) no elimina filas de la tabla de unión y la operación de eliminación falla debido a restricciones de clave externa. Llamar a remove () en User funciona bien (supongo que esto tiene algo que ver con el lado propietario de la relación).

Entonces, ¿cómo puedo eliminar un grupo en este caso?

La única forma que se me ocurre es cargar todos los usuarios del grupo, luego, para cada usuario, eliminar el grupo actual de sus grupos y actualizar al usuario. Pero me parece ridículo llamar a update () en cada usuario del grupo solo para poder eliminar este grupo.

rdk
fuente

Respuestas:

86
  • La propiedad de la relación está determinada por el lugar donde coloque el atributo 'mappedBy' en la anotación. La entidad que pones 'mappedBy' es la que NO es el propietario. No hay posibilidad de que ambas partes sean dueñas. Si no tiene un caso de uso de 'eliminar usuario', simplemente puede mover la propiedad a la Groupentidad, ya que actualmente Useres el propietario.
  • Por otro lado, no ha estado preguntando al respecto, pero vale la pena saber una cosa. El groupsy usersno se combinan entre sí. Quiero decir, después de eliminar la instancia de User1 de Group1.users, las colecciones de User1.groups no se cambian automáticamente (lo cual es bastante sorprendente para mí),
  • Con todo, le sugiero que decida quién es el propietario. Digamos que Useres el dueño. Luego, al eliminar un usuario, la relación usuario-grupo se actualizará automáticamente. Pero al eliminar un grupo, debes encargarte de eliminar la relación tú mismo de esta manera:

entityManager.remove(group)
for (User user : group.users) {
     user.groups.remove(group);
}
...
// then merge() and flush()
Grzegorz Oledzki
fuente
Thnx! Tuve el mismo problema y tu solución lo resolvió. Pero debo saber si hay otra forma de solucionar este problema. Produce un código horrible. ¿Por qué no puede ser em.remove (entidad) y eso es todo?
Royi Freifeld
2
¿Está esto optimizado detrás de escena? porque no quiero consultar todo el conjunto de datos.
Ced
1
Este es un enfoque realmente malo, ¿qué pasa si tiene varios miles de usuarios en ese grupo?
Sergiy Sokolenko
2
Esto no actualiza la tabla de relaciones, en la tabla de relaciones aún permanecen las filas sobre esta relación. No probé, pero eso podría causar problemas.
Saygın Doğu
@SergiySokolenko no se ejecutan consultas de base de datos en ese bucle for. Todos ellos se agrupan después de dejar la función Transaccional.
Kilves
42

Lo siguiente funciona para mí. Agregue el siguiente método a la entidad que no es propietaria de la relación (Grupo)

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

Tenga en cuenta que para que esto funcione, el Grupo debe tener una lista actualizada de Usuarios (lo cual no se hace automáticamente). por lo que cada vez que agrega un Grupo a la lista de grupos en la entidad Usuario, también debe agregar un Usuario a la lista de usuarios en la entidad Grupo.

Damián
fuente
1
cascade = CascadeType.ALL no debe establecerse cuando desee utilizar esta solución. ¡De lo contrario, funciona perfecto!
ltsstar
@damian Hola, he usado su solución, pero tengo un error concurrentModficationException.Creo que, como señaló, la razón podría ser que el grupo debe tener una lista actualizada de usuarios. Porque si agrego más de un grupo al usuario, obtengo esta excepción ...
Adnan Abdul Khaliq
@Damian Tenga en cuenta que para que esto funcione, el Grupo debe tener una lista actualizada de Usuarios (lo cual no se hace automáticamente). por lo que cada vez que agrega un Grupo a la lista de grupos en la entidad Usuario, también debe agregar un Usuario a la lista de usuarios en la entidad Grupo. Podría, por favor más detalles sobre esto, dame un ejemplo ,, Thnks
Adnan Abdul Khaliq
27

Encontré una posible solución, pero ... no sé si es una buena solución.

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

He probado esto y funciona. Cuando elimina el rol, también se eliminan las relaciones (pero no las entidades de permiso) y cuando elimina el permiso, las relaciones con el rol también se eliminan (pero no la instancia del rol). Pero estamos mapeando una relación unidireccional dos veces y ambas entidades son las dueñas de la relación. ¿Podría esto causar algunos problemas para hibernar? ¿Qué tipo de problemas?

¡Gracias!

El código anterior es de otra publicación relacionada.

gelatinas
fuente
colección de problemas de actualización?
jelies
12
Esto no es correcto. ManyToMany bidireccional debe tener un solo propietario en la relación. Esta solución está convirtiendo a ambas partes en propietarios y eventualmente resultará en registros duplicados. Para evitar registros duplicados, se deben utilizar Conjuntos en lugar de Listas. Pero, usar Set es solo una solución para hacer algo que no se recomienda.
L. Holanda
4
entonces, ¿cuál es la mejor práctica para un escenario tan común?
SalutonMondo
8

Como alternativa a las soluciones JPA / Hibernate: puede usar una cláusula CASCADE DELETE en la definición de la base de datos de su clave foregin en su tabla de combinación, como (sintaxis de Oracle):

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

De esa forma, el DBMS borra automáticamente la fila que apunta al grupo cuando lo borras. Y funciona si la eliminación se realiza desde Hibernate / JPA, JDBC, manualmente en la base de datos o de cualquier otra manera.

la función de eliminación en cascada es compatible con todos los principales DBMS (Oracle, MySQL, SQL Server, PostgreSQL).

Pierre Henry
fuente
3

Por lo que vale, estoy usando EclipseLink 2.3.2.v20111125-r10461 y si tengo una relación unidireccional @ManyToMany observo el problema que usted describe. Sin embargo, si lo cambio para que sea una relación bidireccional @ManyToMany, puedo eliminar una entidad del lado no propietario y la tabla JOIN se actualiza adecuadamente. Todo esto sin el uso de atributos en cascada.

NBW
fuente
2

Esto funciona para mi:

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

    // Other business logic

    groupRepository.delete(group);
}

Además, marque el método @Transactional (org.springframework.transaction.annotation.Transactional), esto hará todo el proceso en una sesión, ahorra algo de tiempo.

Mehul Katpara
fuente
1

Esta es una buena solución. La mejor parte está en el lado de SQL: el ajuste fino a cualquier nivel es fácil.

Usé MySql y MySql Workbench en Cascade en la eliminación de la CLAVE externa requerida.

ALTER TABLE schema.joined_table 
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
usuario1776955
fuente
Bienvenido a SO. Tómese un momento y mire esto para mejorar su formato y lectura de prueba: stackoverflow.com/help/how-to-ask
petezurich
1

Esto es lo que terminé haciendo. Ojalá alguien lo encuentre ú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
fuente
0

Para mi caso, eliminé el mappedBy y uní tablas 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
fuente
3
No use cascadeType.all en muchos a muchos. Al eliminar, hace que el registro A elimine todos los registros B asociados. Y los registros B eliminan todos los registros A asociados. Y así. Gran no-no.
Josiah
0

Esto me funciona en un problema similar en el que no pude eliminar al usuario debido a la referencia. Gracias

@ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
Yo yo
fuente