Introducción
Por defecto, las operaciones del Entity Manager se aplican únicamente a las entidades proporcionadas como argumento a sus diferentes métodos de operación. Esta operación, por defecto, no se propagará a otras entidades que tienen relación con la entidad que se está modificando. Es bastante probable que si tenemos una entidad nueva y tiene una relación con otra entidad, en ambas se deba operar simultáneamente. El atributo Cascade nos permitirá la propagación en cascada de las operaciones realizadas en la entidad principal.
¿Qué es Cascade en JPA?
Cascade, en JPA o Hibernate, permite simplificar las operaciones en nuestro código Java. Cuando realizamos alguna acción en la entidad objetivo, la misma acción se aplicará automáticamente a sus entidades asociadas.
Las relaciones entre entidades a menudo dependen de la existencia de otra entidad. Esto resulta directamente del mapeo realizado a la base de datos relacional en la que el ORM esté actuando.
Por ejemplo, en una relación Usuario-Dirección, si eliminamos el Usuario, la entidad Dirección no tiene ningún sentido. Cuando eliminamos la entidad Usuario, nuestra entidad Dirección también debería eliminarse. En otro tipo de relaciones similares, por ejemplo Usuario-Login debemos tener más cuidado, eliminar un Usuario NO debería resultar en la eliminación de su Login, ya que puede estar siendo usado en otras relaciones (logs, registros creados por el usuario, etc) o simplemente tener que realizar un borrado lógico (la entidad no se elimina, se deshabilita).
Las operaciones en cascada de JPA / Hibernate, representadas en el enum javax.persistence.CascadeType, son:
- CascadeType.ALL: se aplican todos los tipos de cascada.
- CascadeType.PERSIST: las operaciones de guardado en la base de datos de las entidades padre se propagarán a las entidades relacionadas.
- CascadeType.MERGE: las entidades relacionadas se unirán al contexto de persistencia cuando la entidad propietaria se una.
- CascadeType.REMOVE: las entidades relacionadas se eliminan de la base de datos cuando la entidad propietaria se elimine.
- CascadeType.REFRESH: las entidades relacionadas actualizan sus datos desde la base de datos cuando la entidad propietaria se actualiza.
- CascadeType.DETACH: se separan del contexto de persistencia todas las entidades relacionadas cuando ocurre una operación de separación manual.
Por defecto, no se aplica ninguna operación de cascada.
Hibernate utiliza las mismas operaciones que JPA, añadiendo otras propias.
En este caso, el bloque de código SIN el atributo de Cascade sería:
Login login = new Login (“nalmeida”); Direccion direccion = new Direccion("Las Palmas de Gran Canaria"); Usuario usuario = new Usuario("Néstor Almeida"); usuario.setLogin(login); usuario.setDireccion(direccion); entityManager.persist(login); entityManager.persist(direccion); entityManager.persist(usuario);
Y, aplicando Cascade a la entidad Direccion:
@Entity public class Usuario { // ... @OneToOne(cascade={CascadeType.PERSIST, CascadeType.REMOVE}) Direccion direccion; @OneToOne(cascade={CascadeType.PERSIST}) Login login; // ... }
En el código anterior bastaría con realizar entityManager.persist(usuario);
ya que el resto de entidades también ejecutarían la orden persist al estar en cascada. Si realizamos un entityManager.remove(usuario);
entonces se eliminaría la dirección asociada, pero no el login.
Diferencia entre persist y merge
entityManager.persist(objeto)
- Inserta el objeto Java como un nuevo registro en la base de datos.
- Añade el objeto Java al Entity Manager y pasa a ser administrado por la misma.
entityManager.merge(objeto)
- Busca un objeto Java ya administrado por el Entity Manager con la misma identificación.
- Si existe, actualiza en la base de datos y devuelve el objeto ya administrado.
- Si no existe, inserta el nuevo registro en la base de datos y pasa a ser administrado.
Aunque merge realiza la misma función que persist, la operación persist es más eficiente para insertar un nuevo registro en una base de datos que merge. Por otra parte, con persist no se duplica el objeto original y, lo más importante, con persist te aseguras de que está insertando y no actualizando por error.
El resumen rápido y sencillo es:
- persist –> para insertar un nuevo objeto en la base de datos
- merge –> para actualizar un objeto existente en la base de datos
¿Qué atributos Cascade aplicar en nuestro modelo?
Para un mismo tipo de relación caben muchas posibilidades según los requisitos funcionales del modelo de negocio. No hay una norma escrita para aplicar Cascade, aunque la recomendación general es tener mucho cuidado con las operaciones en cascada en relaciones many-to-many (@ManyToMany) y en entidades compartidas. Cascade es una herramienta más de JPA e Hibernate, y es tarea del programador saber acertar en su uso. Cascade puede tanto facilitarnos la tarea como amargarnos la existencia si no se usa correctamente. Para un mismo tipo de relación caben muchas posibilidades. La respuesta es que depende de la funcionalidad del modelo de nuestra relación.
CascadeType.ALL
Por norma general el atributo ALL se debe intentar evitar a toda costa. La replicación de todas las operaciones en cascada puede jugar en nuestra contra si no sabemos el efecto de las mismas en nuestro modelo.
¿Cuándo utilizar CascadeType.ALL?
En aquellos caso en que aplique el uso de todas las operaciones en cascada. Simplemente nos da legibilidad al código y permite ahorrarnos anotar todos los atributos. No se debe utilizar como comodín ni ponerse en casos de desconocimiento de la funcionalidad de la relación.
CascadeType.PERSIST
El atributo PERSIST propaga la operación de persistencia de una nueva relación principal a sus nuevas relaciones secundarias. Cuando guardamos la entidad Usuario, la entidad de Direccion también se guardará.
¿Cuándo utilizar CascadeType.PERSIST?
Es util en los casos en los que sabemos que al insertar la entidad principal sus secundarias también han de ser insertadas porque se han creado en el mismo momento. Por ejemplo, si al crear un Usuario también hemos creado su Direccion.
CascadeType.MERGE
El atributo MERGE propaga la operación de actualización en la base de datos de una relación principal a sus secundarias. Cuando guardamos la entidad Usuario, la entidad de Direccion también se guardará.
¿Cuándo utilizar CascadeType.MERGE?
Es util en los casos en los que sabemos que al modificar la entidad principal sus secundarias también han de ser modificadas porque se han creado en el mismo momento. Por ejemplo, si al modificar un Usuario también hemos modificado su Direccion.
CascadeType.REMOVE
El atributo REMOVE permite eliminar automáticamente todas las relaciones asociadas a la relación principal cuando esta se elimina.
¿Cuándo utilizar CascadeType.REMOVE?
A priori, la utilización del borrado en cascada, puede parecer habitual. Dependiendo de la complejidad de la relación se podría eliminar la necesidad de ir eliminando las instancias de otras entidades asociadas de forma programática. Sin embargo, aunque es un elemento muy interesante y facilitador, debe utilizarse con mucho cuidado.
Hay sólo dos situaciones en las que un borrado en cascada se puede usar sin problemas: relaciones one-to-one y one-to-many, en donde hay una clara relación de propiedad y la eliminación de la instancia principal debe causar la eliminación automática de sus instancias dependientes.
No se debe utilizar el borrado en cascada con relaciones múltiples many-to-many. Una colección anotada con @ManyToMany asocia dos entidades principales a través de una tabla de combinación, ¡no debemos ni queremos propagar la eliminación de un elemento principal a otro!
OJO: No puede aplicarse ciegamente a todas las relaciones one-to-one o one-to-many porque las entidades dependientes podrían también estar participando en otras relaciones o podrían tener que mantenerse en la base de datos como entidades aisladas por requisitos funcionales.
CascadeType.REFRESH
El atributo REFRESH y el atributo DETACH no suelen ser habituales. Por tanto nos debemos centrar en el resto de atributos de Cascade.
¿Cuándo utilizar CascadeType.REFRESH?
En el caso del atributo REFRESH, si realizamos una operación EntityManager.refresh(objeto) los objetos se vuelven a cargar desde la base de datos. El contenido del objeto gestionado en la memoria se descarta (incluidos los cambios, si los hubiera) y se reemplaza por los datos que se recuperan de la base de datos. Esto podría ser útil para garantizar que la aplicación maneje la versión más actualizada de un objeto de entidad, en caso de que otro EntityManager lo haya cambiado desde que se recuperó. No es algo común en aplicaciones sencillas.
CascadeType.DETACH
¿Cuándo utilizar CascadeType.DETACH?
El uso del atributo DETACH tampoco suele ser de uso común. En el caso de sacar un objeto del contexto de persistencia, de forma manual, entonces sí que deberíamos realizar la operación en cascada para las entidades relacionadas con el mismo. La operación EntityManager.detach(objeto) sólo la saca al objeto del contexto de persistencia, con lo que se cancelan los cambios al no persistirse en base de datos.
Las operaciones en cascada son unidireccionales. Debemos tener en cuenta cual es la entidad propietaria de la relación y dónde se va a actualizar la misma antes de tomar la decisión de poner el elemento en ambos lados. Por ejemplo, cuando definimos un nuevo usuario y una nueva dirección pondremos la dirección en el usuario. El atributo CascadeType se define únicamente en la relación Usuario.
Diferencia entre CascadeType.REMOVE y orphanRemoval
El atributo orphanRemoval no tiene nada que ver con CascadeType.REMOVE, veremos que son operaciones que se complementan.
¿Qué hace orphanRemoval?
orphanRemoval es un atributo específico del ORM que marca la entidad secundaria a eliminar cuando ya no se haga referencia a ella desde la entidad principal. Por defecto se encuentra desactivado, es decir, a false. Tiene especial interés en casos de tratamiento de colecciones, facilitando el borrado de elementos no asociados.
orphanRemoval=true
@Entity public class Usuario { // ... @OneToOne(cascade={CascadeType.REMOVE}, orphanRemoval=true) Direccion direccion; @OneToMany(cascade={CascadeType.REMOVE}, orphanRemoval=true) List<Telefono> telefonos; // ... }
Cuando se elimina un objeto de la entidad Usuario, la operación de eliminación se propaga en cascada al objeto de la entidad Direccion y al listado Telefonos. En este caso, si especificamos orphanRemoval=true y CascadeType.REMOVE el resultado es redundante en cuanto a la operación de eliminación.
La diferencia entre los dos atributos está en la respuesta a la desconexión de una relación. Por ejemplo, si establecemos el objeto de dirección a null o apuntamos a otro objeto Direccion. En el caso de los listados obtendremos el mismo resultado si hacemos lo propio con alguno de los objetos Telefono o la lista en sí.
Direccion dirActual = usuario.getDireccion(); // dirActual queda desconectada de usuario // Con null en el atributo direccion usuario.setDireccion(null); // O estableciendo otro objeto usuario.setDireccion(new Direccion(“Otra dirección”)); // dirActual pasa a ser huérfano (orphan) List<Telefono> telefonos = usuario.getTelefonos(); // Expresión regular para teléfonos móviles españoles // Eliminamos todos los teléfonos que no cumplan la expresión regular for (Telefono telefono : telefonos) { if (!telefono.getMovil().matches(“(\+34|0034|34)?[ -]*(6|7)[ -]*([0-9][ -]*){8}”)) { telefonos.remove(telefono); } } // los teléfonos eliminados pasan a ser huérfanos (orphans) entityManager.merge(usuario);
Si se especifica orphanRemoval=true, las instancia desconectadas se eliminan automáticamente si otros objetos no siguen apuntándolas. En caso de que orphanRemoval no esté activo, se quedan en el limbo de los objetos. En la base de datos se quedarán los registros desasociados.
IMPORTANTE: orphanRemoval implica que no debes cambiar a los objetos dependientes su padre. La norma general es establecer orphanRemoval=true siempre que esté seguro de que las relaciones de esa entidad no migrarán a una entidad diferente.
Por ejemplo, si deseamos intercambiar los teléfonos del usuario A al usuario B.
// Obtenemos los teléfonos List<Telefono> telUsuarioA = usuarioA.getTelefonos(); List<Telefono> telUsuarioB = usuarioB.getTelefonos(); // Los eliminamos de los usuarios usuarioA.getTelefonos().clear(); usuarioB.getTelefonos().clear(); // OJO: si se produce un flush() o commit() en este momento se pierden todos los teléfonos! usuarioA.getTelefonos().addAll(telUsuarioB); usuarioB.getTelefonos().addAll(telUsuarioA); entityManager.merge(usuarioA); entityManager.merge(usuarioB);
Como vemos, orphanRemoval=true es útil para limpiar objetos dependientes que no deberían existir sin una referencia de un objeto propietario.
orphanRemoval=false
@Entity public class Usuario { // ... @OneToOne(cascade={CascadeType.REMOVE}, orphanRemoval=false) Direccion direccion; @OneToMany(cascade={CascadeType.REMOVE}, orphanRemoval=false) List<Telefono> telefonos; // ... }
Si solo establecemos el atributo CascadeType.REMOVE, no se realiza ninguna acción automática de limpieza de entidades Direccion huérfanas. Desconectar una relación no es una operación de eliminación. En caso de eliminar la entidad Usuario también se eliminará, por cascada, la entidad Direccion asociada y el listado de Telefonos.
Es decir, no es necesario aplicar orphanRemoval si solo eliminamos aplicando Cascade, sería solo necesario si hay de por medio operaciones de desconexión, se aplique Cascade o no.
@Entity public class Usuario { // ... @OneToOne(orphanRemoval=false) Direccion direccion; @OneToMany(orphanRemoval=false) List<Telefono> telefonos; // ... }
Finalmente, si no establecemos ni orphanRemoval ni CascadeType.REMOVE entonces no se eliminan las entidades asociadas (Direccion y lista Telefono) al aplicar el borrado de la entidad Usuario. Tampoco se eliminarían en caso de desconectarlas de Usuario. En este caso se quedan desasociadas en la base de datos en el limbo de los objetos y registros de la base de datos.
Nota: orphanRemoval está desactivado por defecto, no es necesario añadir orphanRemoval=false
Ejemplos de uso básico
Hemos tenido en cuenta los siguientes requisitos funcionales:
- Si creamos un usuario también se crea la dirección asociada al mismo. Esto mismo se aplica a la actualización del usuario, si ha cambiado algo en la dirección también se actualiza.
- Si borramos un usuario solo se borra la dirección, quedando vigente el login asociado al mismo.
- Por defecto no se permite el borrado de un login ya que podría estar siendo usado en el resto de relaciones. En caso de poder borrar el login no se eliminan sus roles asociados.
- En caso de crear un rol se persisten los permisos, al eliminar el rol también se borran dichos permisos. Una actualización de los permisos asociados al rol no afecta a la actualización de cada permiso en si.
Para determinar las relaciones en cascada debemos plantearnos todas las posibles combinaciones sobre la entidad. Como adelantamos, esto depende de su función por lo que debemos elegir de forma precisa según el modelo.
Vamos a tener en cuenta, sobre todo, las operaciones en cascada de PERSIST, MERGE y REMOVE. Evitaremos utilizar ALL como comodín y se obviarán las operaciones REFRESH y DETACH por ser casos poco comunes.
Para activar la persistencia en cascada debemos añadir el atributo cascade={CascadeType.OPERACION_1, …, CascadeType.OPERACION_N} en la declaración de la relación.
Usuario-Dirección
Es decir que un usuario tiene una dirección y esta dirección tiene un único usuario. Debemos tener en cuenta estos aspectos:
• ¿Si insertamos el usuario deberíamos insertar también la dirección? SI, si no existe → PERSIST
• ¿Si actualizamos el usuario deberíamos actualizar también la dirección? SI, si ha cambiado → MERGE
• ¿Si borramos el usuario deberíamos borrar también la dirección? SI, es única → REMOVE
@Entity public class Usuario { // ... @OneToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) Direccion direccion; // ... }
Usuario-Login
En este caso un usuario tiene un login único y este login pertenece a un único usuario. Debemos tener en cuenta estos aspectos:
• ¿Si insertamos el usuario deberíamos insertar también el login? NO, ya debe existir
• ¿Si actualizamos el usuario deberíamos actualizar también el login? NO, no permite cambios
• ¿Si borramos el usuario deberíamos borrar también su login? NO, no permite borrado
@Entity public class Usuario { // ... @OneToOne Login login; // ... }
Login-Rol
Un login tiene asociado un conjunto de roles y estos roles pueden compartirse entre varios logins. Debemos tener en cuenta estos aspectos:
• ¿Si insertamos el login deberíamos insertar también los roles? NO, ya existen
• ¿Si actualizamos el login deberíamos actualizar también los roles? NO, no permiten cambios
• ¿Si borramos el login deberíamos borrar también sus roles? NO, están siendo compartidos
@Entity public class Login { // ... @ManyToMany Set<Rol> roles; // ... }
Rol-Permiso
En este caso un rol dispone de un conjunto de permisos que son únicos para el mismo. Debemos tener en cuenta estos aspectos:
• ¿Si insertamos el rol deberíamos insertar también los permisos? SI → PERSIST
• ¿Si actualizamos el rol deberíamos actualizar también los permisos? NO, no es necesario
• ¿Si borramos el rol deberíamos borrar también sus permisos? SI, son únicos
@Entity public class Rol { // ... @ManyToOne(cascade={CascadeType.PERSIST, CascdeType.REMOVE}) Set<Permiso> permisos; // ... }
Más artículos como estos por favor, me ayudo mucho esta información, gracias infinitas!!.
😛 Me alegro muchísimo. Un placer haberte podido ayudar!
Magistral la guía, de verdad muchas gracias por que por fin he podido entender las funcionalidades de cascade.
Me alegro mucho que te haya sido de utilidad. No dudes en realizar alguna pregunta si te queda alguna duda, así amplío el artículo. 🙂
Muy buenas, estoy teniendo un problema, tengo varias relaciones oneToMany, sucede q si elimino de la primera lista en orden de como se declaro en la entity funciona bien, pero si elimino una lista de otra lista me da error detached entity passed to persist y solo puedo eliminar si la lista de arriba la borro completa
Muchas gracias, me aclaraste todas mis dudas.