Soft Delete en Hibernate y Spring Boot
By Noel Rodríguez Calle mayo 20, 2020
En este nuevo artículo de Refactorizando, vamos a ver la funcionalidad Soft Delete
en Hibernate con un ejemplo en Spring Boot, mediante el uso de anotaciones.
Para ello haremos uso de @SQLDelete
¿Qué es un Soft Delete?
Podríamos definir un softdelete como un borrado lógico de un registro de una
tabla, mediante una actualización de un campo. Para ello ese registro debe tener
un campo, que se por lo general, borrado.
Ejemplo Soft Delete
Definir el objeto de Base de Datos
1. package com.refactorizando.soft.delete;
2.
3. import javax.persistence.Entity;
4. import javax.persistence.GeneratedValue;
5. import javax.persistence.Id;
6. import javax.persistence.Table;
7.
8. import lombok.Getter;
9. import lombok.Setter;
10.
11. @Entity
12. @Getter
13. @Setter
14. @Table(name = "car")
15. public class car {
16.
17. @Id
18. @GeneratedValue
19. private Long id;
20.
21. private String model;
22.
23. private String color;
24.
25. private Boolean deleted;
26. }
Hemos definido un campo borrado que será el que se actualice y hará de borrado
lógico.
Definición del Controlador
A continuación vamos a definir los métodos de nuestra API:
1. package com.refactorizando.soft.delete;
2.
3. import org.springframework.http.HttpStatus;
4. import org.springframework.http.ResponseEntity;
5. import org.springframework.web.bind.annotation.DeleteMapping;
6. import org.springframework.web.bind.annotation.GetMapping;
7. import org.springframework.web.bind.annotation.PostMapping;
8. import org.springframework.web.bind.annotation.RequestBody;
9. import org.springframework.web.bind.annotation.RestController;
10.
11. import lombok.RequiredArgsConstructor;
12.
13. @RestController
14. @RequiredArgsConstructor
15. public class carController {
16.
17. private final CarService carService;
18.
19. @PostMapping(value = "/save")
20. public ResponseEntity<Car> save(@RequestBody Car car) {
21. return new ResponseEntity<>(carService.save(car), HttpStatus.OK);
22. }
23. @DeleteMapping("/delete/{id}")
24. public void delete(@PathVariable Long id) {
25. carService.delete(id);
26. }
27. @GetMapping(value = "/list")
28. public ResponseEntity<List<Car>> findAll() {
29. List<Car> cars = carService.findAll();
30. return new ResponseEntity<>(cars, HttpStatus.OK);
31. }
32. }
@SQLDelete
@SQLDelete es una anotación proporcionada por Hibernate que nos permite realizar
un borrado lógico cuando el método delete de JPA es invocado.
Esta anotación puede recibir tres parámetros de entrada:
sql: Procedure name or SQL UPDATE/DELETE statement.
callable:Is the statement callable (aka a CallableStatement)
check: For persistence operation what style of determining results (success/failure) is
to be used.
A continuación vamos a aplicar esta anotación a nuestra clase:
1. package com.refactorizando.soft.delete;
2.
3. import javax.persistence.Entity;
4. import javax.persistence.GeneratedValue;
5. import javax.persistence.Id;
6. import javax.persistence.Table;
7.
8. import lombok.Getter;
9. import lombok.Setter;
10.
11. @Entity
12. @Getter
13. @Setter
14. @Table(name = "car")
15. @SQLDelete(sql = "UPDATE car SET deleted=true WHERE id = ?")
16. public class car {
17.
18. @Id
19. @GeneratedValue
20. private Long id;
21.
22. private String model;
23.
24. private String color;
25.
26. private Boolean deleted;
27. }
Hemos creado un update del campo „deleted‟ y cada vez que el método delete(T) sea
invocado se ejecutará el update en lugar del borrado. El método delete pertenece a la
interfaz de CrudRepository.
Si invocamos de nuestra API el método delete y a continuación el método findAll()
obtenemos:
Salida:
1. [
2. {
3. "id" : 1,
4. "model": "MAZDA",
5. "color": "RED",
6. "deleted": "false",
7. },
8. {
9. "id" : 2,
10. "model": "SEAT",
11. "color": "BLUE",
12. "deleted": "true",
13. }
14. ]
Es decir, nos ha mostrado todos los registros de nuestra base de datos, pero si
tenemos un registro borrado no debería de aparecer, así que para eso tenemos la
anotación @Where
@Where
Esta anotación nos va a permitir establecer un filtro a la hora de mostrar nuestro
objeto:
1. package com.refactorizando.soft.delete;
2.
3. import javax.persistence.Entity;
4. import javax.persistence.GeneratedValue;
5. import javax.persistence.Id;
6. import javax.persistence.Table;
7.
8. import lombok.Getter;
9. import lombok.Setter;
10.
11. @Entity
12. @Getter
13. @Setter
14. @Table(name = "car")
15. @SQLDelete(sql = "UPDATE car SET deleted=true WHERE id = ?")
16. @Where(clause = "deleted = false")
17. public class car {
18.
19. @Id
20. @GeneratedValue
21. private Long id;
22.
23. private String model;
24.
25. private String color;
26.
27. private Boolean deleted;
28. }
Con sentencia @Where(clause = «deleted = false»), tendremos los siguientes
resultados:
Salida:
1. [
2. {
3. "id" : 1,
4. "model": "MAZDA",
5. "color": "RED",
6. "deleted": "false",
7. }
8. ]
Mostrar registros con Soft Delete de manera dinámica.
A veces tenemos que mostrar los registros borrados o los no borrados en función
de alguna premisa, para ello podemos usar @Filter y @FilterDef.
@FilterDef
Esta anotación define los requerimientos, los cuales, serán usados por @Filter:
name: El nombre del filtro que será usado en @Filter y en session (lo veremos
en el ejemplo)
parameters: Se definen los parametros usando @ParamDef, en donde se
define el nombre y el tipo.
@Filter
@Filter usará la definición que se ha hecho en la anotación @FilterDef usando el
nombre del parámetro definido. Recibe los siguiente parámetros:
name: El nombre de que se ha definido en @FilterDef
condition: Condición para aplicar el filtro en función del parámetro.
Veamos con un ejemplo como funcionaría @Filter. La idea es pasar por parámetro de
nuestro controlador que nos muestre los borrados o no. Para ello vamos a modificar la
clase Car añadiendo @Filter y el método findAll(). Y añadir un nuevo método para
meter en Session el filtro definido con el parámetro que llega.
1. @Entity
2. @Getter
3. @Setter
4. @Table(name = "car")
5. @SQLDelete(sql = "UPDATE car SET deleted=true WHERE id = ?")
6. @FilterDef(
7. name = "deletedCarFilter",
8. parameters = @ParamDef(name = "isDeleted", type = "boolean")
9. )
10. @Filter(
11. name = "deletedCarFilter",
12. condition = "deleted = :isDeleted"
13. )
14. public class car {
15. ...
16. }
Hemos eliminado la anotación @Where porque no es compatible el uso de ambas
anotaciones.
1. @GetMapping(value = "/list")
2. public ResponseEntity<List<Car>> findAll(@RequestParam(value =
3. "isDeleted", required = false, defaultValue = "false")boolean
4. isDeleted {
5.
6. List<Car> cars = carService.findAllFilter(isDeleted);
7.
8. return new ResponseEntity<>(cars, HttpStatus.OK);
9. }
10.
11. public List<Car> findAllFilter(boolean isDeleted) {
12. Session session = entityManager.unwrap(Session.class);
13. Filter filter = session.enableFilter("deletedCarFilter");
14. filter.setParameter("isDeleted", isDeleted);
15. List<Car> cars = carRepository.findAll();
16. session.disableFilter("deletedCarFilter");
17. return cars;
18. }
Si ejecutamos, ahora el método findAll(), en función de parámetros obtendremos:
Ejecución:
localhost:8080/cars/list/?isDeleted=true
Salida:
1. [
2. {
3. "id" : 2,
4. "model": "SEAT",
5. "color": "BLUE",
6. "deleted": "true",
7. }
8. ]
Ejecución:
localhost:8080/cars/list/?isDeleted=false
Salida:
1. [
2. {
3. "id" : 1,
4. "model": "MAZDA",
5. "color": "RED",
6. "deleted": "false",
7. }
8. ]
@PrePersist
public void prePersist() {
deleted= false;
}
Conclusión
En esta entrada hemos visto cómo funciona el Soft Delete en Hibernate y Spring
Boot, para evitar tener que hacer actualizaciones de nuestro objeto del que
queremos hacer borrados lógicos.
Cómo implementar una eliminación suave con
Hibernate
No es tan difícil implementar una eliminación suave con Hibernate. Sólo tienes
que:
1. Decirle a Hibernate que realice una ACTUALIZACIÓN SQL en lugar de una
operación ELIMINAR y
2. Excluya todos los registros "eliminados" de los resultados de su consulta.
Te mostraré cómo puedes hacer eso fácilmente en esta publicación. Todos los
ejemplos usarán la siguiente entidad Cuenta que usa la enumeración
AccountState para indicar si una cuenta está INACTIVA, ACTIVA o ELIMINADA.
@Entity
@NamedQuery(name = ‚Account.FindByName‛, query = ‚SELECT a FROM Account a WHERE name like :name‛)
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = ‚id‛, updatable = false, nullable = false)
private Long id;
@Column
private String name;
@Column
@Enumerated(EnumType.STRING)
private AccountState state;
…
}
Actualice el registro en lugar de eliminarlo
Para implementar una eliminación temporal, debe anular la operación de
eliminación predeterminada de Hibernate. Puede hacerlo con una anotación
@SQLDelete. Esta anotación le permite definir una consulta SQL nativa
personalizada que Hibernate ejecutará cuando elimine la entidad. Puede ver un
ejemplo de ello en el siguiente fragmento de código.
@Entity
@SQLDelete(sql = ‚UPDATE account SET state = ‘DELETED’ WHERE id = ?‛, check = ResultCheckStyle.COUNT)
public class Account { … }
La anotación @SQLDelete en el fragmento de código anterior le dice a Hibernate
que ejecute la instrucción SQL UPDATE dada en lugar de la instrucción SQL
DELETE predeterminada. Cambia el estado de la cuenta a DELETED y puede
usar la propiedad del estado en todas las consultas para excluir las cuentas
eliminadas.
Account a = em.find(Account.class, a.getId());
em.remove(a);
16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name
as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where
account0_.id=? and ( account0_.state <> „DELETED‟)
16:07:59,534 DEBUG SQL:92 – UPDATE account SET state = „DELETED‟
WHERE id = ?
Eso es todo lo que necesita hacer para crear una implementación básica de
eliminación suave. Pero hay otras 2 cosas que debes manejar:
1. Cuando borras una entidad de Cuenta, Hibernate no actualiza el valor de su
atributo de estado en la sesión actual.
2. Debe adaptar todas las consultas para excluir las entidades eliminadas.
Actualizar la propiedad del estado en la sesión actual
Hibernate no analiza la consulta nativa que le proporcionas a la anotación
@SQLDelete. Simplemente establece los valores de los parámetros de vinculación
y los ejecuta. Por lo tanto, no sabe que proporcionó una instrucción SQL UPDATE
en lugar de una instrucción DELETE a la anotación @SQLDelete. Tampoco sabe
que el valor del atributo de estado está desactualizado después de realizar la
operación de eliminación.
En la mayoría de los casos, esto no es un problema. Tan pronto como Hibernate
ejecuta la declaración SQL, el registro de la base de datos se actualiza y todas las
consultas usan el nuevo valor de estado. Pero, ¿qué pasa con la entidad Cuenta
que proporcionó a la operación EntityManager.remove (entidad Objeto)?
La propiedad estatal de esa entidad está desactualizada. Eso no es gran cosa si
libera la referencia inmediatamente después de eliminarla. En todos los demás
casos, debe actualizar el atributo usted mismo.
La forma más sencilla de hacerlo es utilizar una devolución de llamada del ciclo de
vida, como hago en el siguiente fragmento de código. La anotación @PreRemove
en el método deleteUser le dice a Hibernate que llame a este método antes de
realizar la operación de eliminación. Lo uso para establecer el valor de la
propiedad estatal en DELETED.
@Entity
@SQLDelete(sql = ‚UPDATE account SET state = ‘DELETED’ WHERE id = ?‛, check = ResultCheckStyle.COUNT)
public class Account {
…
@PreRemove
public void deleteUser() {
this.state = AccountState.DELETED;
}
}
Excluir entidades "eliminadas" en consultas
Debe verificar el atributo de estado en todas las consultas para excluir los registros
eliminados de la base de datos de los resultados de la consulta. Esta es una tarea
propensa a errores si la hace manualmente y le obliga a definir todas las consultas
usted mismo. El método EntityManager.find (Class entityClass, Object primaryKey)
y los métodos correspondientes en la sesión de Hibernate no conocen la
semántica del atributo de estado y no la tienen en cuenta.
La anotación @Where de Hibernate proporciona una mejor manera de excluir
todas las entidades eliminadas. Permite definir un fragmento de código SQL que
Hibernate agrega a la cláusula WHERE de todas las consultas. El siguiente
fragmento de código muestra una anotación @Where que excluye un registro si su
estado es ELIMINADO.
@Entity
@SQLDelete(sql = ‚UPDATE account SET state = ‘DELETED’ WHERE id = ?‛, check = ResultCheckStyle.COUNT)
@Where(clause = ‚state <> ‘DELETED'‛)
@NamedQuery(name = ‚Account.FindByName‛, query = ‚SELECT a FROM Account a WHERE name like :name‛)
public class Account { … }
Como puede ver en los siguientes fragmentos de código, Hibernate agrega la
cláusula WHERE definida cuando realiza una consulta JPQL o llama al método
EntityManager.find (Class entityClass, Object primaryKey).
1. TypedQuery<Account> q = em.createNamedQuery(“Account.FindByName”,
Account.class);
2. q.setParameter(“name”, “%ans%”);
3. Account a = q.getSingleResult();
4. 16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_,
account0_.name as name2_0_, account0_.state as state3_0_ from Account
account0_ where ( account0_.state <> „DELETED‟) and (account0_.name
like ?)
Resumen
Como ha visto, es bastante sencillo implementar una eliminación suave con
Hibernate. Solo tiene que usar una anotación @SQLDelete para definir una
declaración SQL personalizada para la operación de eliminación. También debe
usar la anotación @Where de Hibernate para definir un predicado que excluya
todos los registros eliminados por defecto.