Tipos de Cascade en Hibernate-JPA
Uno de los problemas o dudas que nos suelen surgir cuando trabajamos con Spring Data e Hibernate es saber los diferentes tipos de Cascade en Hibernate-JPA y cuál aplicar. En este artículo vamos a ver los diferentes tipos que tenemos y cuándo y cómo aplicar los tipos de Cascade en Hibernate.
Diferentes tipos de Cascade en Hibernate-JPA
A continuación se enumeran todos los tipos de cascade que puedes encontrar en Hibernate -JPA:
- ALL
- PERSIST
- MERGE
- REMOVE
- REFRESH
- DETACH
Una vez enumerados los diferentes tipos vamos a comenzar con su descripción.
CascadeType.ALL en Hibernate-JPA
El tipo CascadeType.ALL va a realizar una propagación de todas las operaciones desde el padre a los hijos. Es decir, incluye todas las operaciones.
Vamos a ver un ejemplo con una relación OneToMany:
@Entity @Table(name = "ADDRESS") @Getter @Setter @NoArgsConstructor public class AddressEntity extends Audit { @Id @GeneratedValue @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "ADDRESS_ID") private UUID addressId; @Column(name = "ADDRESS_NAME") private String name; @Column private String country; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "FK_USER") private UserEntity user; }
@Getter @Setter @NoArgsConstructor @Entity @Table(name = "USER") public class UserEntity extends Audit { @Id @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "USER_ID") @GeneratedValue private UUID userId; @Column private String name; @OneToMany(mappedBy = "asset", orphanRemoval = true, cascade = { CascadeType.ALL }, fetch = FetchType.LAZY) private Set<AddressEntity> addresses = new HashSet<>(); }
Con esta aproximación realizaríamos una propagación de User hacia las entidades hijas (AddressEntity) de todas las operaciones.
Aplicaremos CascadeType.ALL cuando se quiere realizar una propagación de todas las operaciones que nos ofrece JPA.
CascadeType.PERSIST en Hibernate-JPA
El cascadeType.Persiste se realizará cuando se quiere realizar la propagación de persistencia hacia los hijos, es decir, cualquier operación de guardado en el padre será transmitido al hijo que también será guardado.
Por ejemplo haciendo uso de las entidades anteriores:
@Getter @Setter @NoArgsConstructor @Entity @Table(name = "USER") public class UserEntity extends Audit { @Id @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "USER_ID") @GeneratedValue private UUID userId; @Column private String name; @OneToMany(mappedBy = "asset", orphanRemoval = true, cascade = { CascadeType.PERSIST }, fetch = FetchType.LAZY) private Set<AddressEntity> addresses = new HashSet<>(); }
@Entity @Table(name = "ADDRESS") @Getter @Setter @NoArgsConstructor public class AddressEntity extends Audit { @Id @GeneratedValue @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "ADDRESS_ID") private UUID addressId; @Column(name = "ADDRESS_NAME") private String name; @Column private String country; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "FK_USER") private UserEntity user; }
Al ejecutar el caso anterior se producirá un guardado en Base de Datos tanto de User como de Address, pero no se realizarán más operaciones que la de persistencia.
CascadeType.REMOVE en Hibernate-JPA
El tipo de Cascade Remove elimina el elemento de la Base de Datos y del contexto, además propaga hacia los hijos la operación. Es decir, si eliminamos un padre eliminaremos el hijo o hijos.
Por ejemplo, para aplicarlo en el caso anterior tendremos que modificar el type.
@Entity @Table(name = "ADDRESS") @Getter @Setter @NoArgsConstructor public class AddressEntity extends Audit { @Id @GeneratedValue @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "ADDRESS_ID") private UUID addressId; @Column(name = "ADDRESS_NAME") private String name; @Column private String country; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "FK_USER") private UserEntity user; }
@Getter @Setter @NoArgsConstructor @Entity @Table(name = "USER") public class UserEntity extends Audit { @Id @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "USER_ID") @GeneratedValue private UUID userId; @Column private String name; @OneToMany(mappedBy = "asset", orphanRemoval = true, cascade = { CascadeType.REMOVE }, fetch = FetchType.LAZY) private Set<AddressEntity> addresses = new HashSet<>(); }
Ahora vamos a aplicar un test para ver el resultado. Por ejemplo si intentamos guardar, intentará previamente ver que el padre existe sino dará error:
@Test void given_user_when_request_to_save_then_return_error() { Throwable exception = assertThrows(JpaObjectRetrievalFailureException.class, () -> userRepository.save(this.user)); assertEquals("org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.refactorizando.example.jpa.data.entity.UserEntity with id 89ce9933-bce0- 4018-97d0-9b481c568ea3; nested exception is javax.persistence.EntityNotFoundException: com.refactorizando.example.jpa.data.entity.UserEntity with id 89ce9933-bce0-4018- 97d0-9b481c568ea3", exception.getMessage()); }
Es decir, esta preparado para realizar un borrado en cascada pero no para realizar una insercción en cascada, al intentar guardar, lo que mira el hijo es ver si existe el padre. Si el padre no existe previamente no podrá realizar la insercción.
CascadeType.MERGE en Hibernate-JPA
Cuando hacemos uso del tipo de Cascada Merge en nuestras entidades, la idea es que el estado del objeto que vamos a guardar se copie con el mismo identificador al objeto persistente.
Cuando hacemos uso de Merge como propiedad de CascadeType, la operación se propaga al hijo desde el padre.
Vamos a usar CascadeType.Merge, por ejemplo cuando no hagamos uso de @GeneratedValue para autogenerar nuestros Id’s y lo generamos a manos o hacemos uso por ejemplo de prepersist. O también puede ser usado cuando modificamos un valor el objeto y a continuación al hacer merge estaría persistido, es decir, modificamos un objeto y haciendo uso de session haríamos merge sobre el objeto que hemos modificado.
Vamos a utilizar el ejemplo anterior pero vamos a eliminar los @GeneratedValue para ver su funcionamiento:
@Entity @Table(name = "ADDRESS") @Getter @Setter @NoArgsConstructor public class AddressEntity extends Audit { @Id @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "ADDRESS_ID") private UUID addressId; @Column(name = "ADDRESS_NAME") private String name; @Column private String country; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "FK_USER") private UserEntity user; }
@Getter @Setter @NoArgsConstructor @Entity @Table(name = "USER") public class UserEntity extends Audit { @Id @Type(type = "org.hibernate.type.UUIDCharType") @Column(name = "USER_ID") private UUID userId; @Column private String name; @OneToMany(mappedBy = "asset", orphanRemoval = true, cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY) private Set<AddressEntity> addresses = new HashSet<>(); }
Vamos a realizar un test para guardar tanto el hijo como el padre:
@Test void given_user_when_request_to_save_with_merge_then_return_user() { UserEntity user = new UserEntity(); user.setUserId(UUID.randomUUID()); AddressEntity address = new AddressEntity(); Set<AddressEntity> addressEntities = new HashSet<>(); addressEntities.add(address); address.setAddressId(UUID.randomUUID()); asset.setAddresses(addressEntities); UserEntity userSaved = assetRepository.save(asset); assertTrue(user.getUserId().equals(userSaved.getUserId()); }
En el ejemplo anterior hemos podido persistir el objeto que hemos creado, vamos a ver otro ejemplo de uso de una manera más programática:
@Test public void given_a_parent_when_merge_then_is_saved() { UserEntity user = new UserEntity(); user.setName("Noel"); Address address = new AddressEntity(); address.setStreet("Atocha"); user.setAddresses(Arrays.asList(address)); session.persist(user); session.flush(); UUID addressId = address.getId(); session.clear(); AddressEntity savedAddressEntity = session.find(AddressEntity.class, addressId); UserEntity savedUser = savedAddressEntity.getUser(); savedUser.setName("Noel Rodríguez"); savedUser.setNumber(9); session.merge(savedUser); session.flush(); }
Como podemos ver en el ejemplo anterior el uso de merge nos ha servido para persistir la información que tenemos en la session, por ejemplo, hemos podido hacer un cambio y guardarlo. Ese cambio se ha traducido en Hibernate mediante un update en BBDD.
CascadeType.DETACH en Hibernate-JPA
La operación de cascada tipo DETACH, lo que va a realizar es eliminar la entidad del contexto persistido ( EntityManager). Es decir, el uso de DETACH va a eliminar de la session de Hibernate (HibernateUtil.getSessionFactory()), el elemento al que se le aplica DETACH.
Los hijos del padre también serán afectados y eliminados del contexto.
Usaremos DETACH cuando lo que queremos es hacer alguna verificación (Sanity Check) o trabajar con el objeto sin afectar a la Base de Datos.
¿Qué sucede si intentamos guardar un Objeto Detach en Base de Datos? Al intentar guardar un objeto al que hemos hecho previamente DETACH obtendremos un error de Hibernate como este: org.hibernate.PersistentObjectException: detached entity passed to persist. Para poder guardar en Base de Datos un objeto tendremos que hacer merge para que ese objeto se copie al contexto persistente y así poder guardarlo en BBDD.
Uso de CascadeType.REFRESH en Hibernate-JPA
El uso de REFRESH como tipo de cascada en Hibernate-JPA se va a encargar de re-leer el valor o elemento de una instancia de la Base de Datos. El uso de Refresh nos va a permitir realizar cambios después de guardar en base de datos y deshacerlos después sin que tenga efectos en Base de Datos. Al hacer uso de REFRESH también los hijos se cargarán de la Base de Datos.
Si tenemos en cuenta las entidades creadas anteriormente podemos aplicar REFRESH para ver el resultado:
@Test public void given_a_parent_when_refresh_then_is_not_saved() { UserEntity user = new UserEntity(); user.setName("Noel"); Address address = new AddressEntity(); address.setStreet("Atocha"); user.setAddresses(Arrays.asList(address)); session.persist(user); session.flush(); user.setName("Noel Rodríguez"); address.setNumber(9); session.refresh(user); assertTrue(user.getName()).isEqualTo("Noel"); }
Como podemos ver en el ejemplo, lo que hace refresh es guardar el objeto de Base de Datos para poder hacer uso de ese mismo objeto más adelante, volviendo al estado original de la Base de Datos.
Conclusión
En este artículo hemos visto los diferentes tipos de Cascade en Hibernate-JPA, así como su uso y posibles problemas que podemos llegar a tener. El entender y conocer los tipos de Cascada en nuestra aplicación nos va a ayudar a evitar problemas de persistencia en nuestra Base de Datos.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!