Soft Delete en Hibernate y Spring Boot


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 soft delete 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

package com.refactorizando.soft.delete;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "car")
public class car {

  @Id
  @GeneratedValue
  private Long id;
  
  private String model;
  
  private String color;
  
  private Boolean deleted;
}

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:

package com.refactorizando.soft.delete;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class carController {

  private final CarService carService;
  
  @PostMapping(value = "/save")
  public ResponseEntity<Car> save(@RequestBody Car car) {
    return new ResponseEntity<>(carService.save(car), HttpStatus.OK);
  }
  @DeleteMapping("/delete/{id}")
  public void delete(@PathVariable Long id) {
    carService.delete(id);
  }
  @GetMapping(value = "/list")
  public ResponseEntity<List<Car>> findAll() {
    List<Car> cars = carService.findAll();
    return new ResponseEntity<>(cars, HttpStatus.OK);
  }
}

@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:

package com.refactorizando.soft.delete;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "car")
@SQLDelete(sql = "UPDATE car SET deleted=true WHERE id = ?")
public class car {

  @Id
  @GeneratedValue
  private Long id;
  
  private String model;
  
  private String color;
  
  private Boolean deleted;
}

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:

[
  {
   "id" : 1,
   "model": "MAZDA",
   "color": "RED",
   "deleted": "false",
  },
  {
   "id" : 2,
   "model": "SEAT",
   "color": "BLUE",
   "deleted": "true",
  }
]

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:

package com.refactorizando.soft.delete;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "car")
@SQLDelete(sql = "UPDATE car SET deleted=true WHERE id = ?")
@Where(clause = "deleted = false")
public class car {

  @Id
  @GeneratedValue
  private Long id;
  
  private String model;
  
  private String color;
  
  private Boolean deleted;
}

Con sentencia @Where(clause = «deleted = false»), tendremos los siguientes resultados:

Salida:

[
  {
   "id" : 1,
   "model": "MAZDA",
   "color": "RED",
   "deleted": "false",
  }  
]

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.

@Entity
@Getter
@Setter
@Table(name = "car")
@SQLDelete(sql = "UPDATE car SET deleted=true WHERE id = ?")
@FilterDef(
        name = "deletedCarFilter",
        parameters = @ParamDef(name = "isDeleted", type = "boolean")
)
@Filter(
        name = "deletedCarFilter",
        condition = "deleted = :isDeleted"
)
public class car {
...
}

Hemos eliminado la anotación @Where porque no es compatible el uso de ambas anotaciones.

@GetMapping(value = "/list")
public ResponseEntity<List<Car>> findAll(@RequestParam(value = 
    "isDeleted", required = false, defaultValue = "false")boolean 
     isDeleted {

    List<Car> cars = carService.findAllFilter(isDeleted);

    return new ResponseEntity<>(cars, HttpStatus.OK);
}

public List<Car> findAllFilter(boolean isDeleted) {
    Session session = entityManager.unwrap(Session.class);
    Filter filter = session.enableFilter("deletedCarFilter");
    filter.setParameter("isDeleted", isDeleted);
    List<Car> cars = carRepository.findAll();
    session.disableFilter("deletedCarFilter");
    return cars;
}

Si ejecutamos, ahora el método findAll(), en función de parámetros obtendremos:

Ejecución:

localhost:8080/cars/list/?isDeleted=true

Salida:

[  
  {
   "id" : 2,
   "model": "SEAT",
   "color": "BLUE",
   "deleted": "true",
  }
]

Ejecución:

localhost:8080/cars/list/?isDeleted=false

Salida:

[
  {
   "id" : 1,
   "model": "MAZDA",
   "color": "RED",
   "deleted": "false",
  }  
]

Conclusión

En esta entrada hemos visto como 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.


1 pensamiento sobre “Soft Delete en Hibernate y Spring Boot

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *