Spring Boot: Soft Delete functionality with Hibernate

Patel Romil
Level Up Coding
Published in
5 min readFeb 22, 2020

--

In the previous article, we have discussed the Web and Stereotype annotations and saw the examples with code. In this article, I will demonstrate the basic soft delete functionality using the annotations.

To implement Soft Delete functionality we will use @SQLDelete, @Where, @Filter, and @FilterDef. To generate the common getters-setters we will use @Data as follow.

@Data

A shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non-final fields, and @RequiredArgsConstructor.

To use this, you have to add the following dependency in pom.xml or build.gradle and add Lombok plugin for your IntelliJ IDEA, Eclipse.

//For Maven Project
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
//For Gradle Project
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.12'
annotationProcessor 'org.projectlombok:lombok:1.18.12'

testCompileOnly 'org.projectlombok:lombok:1.18.12'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'
}

Let’s start by creating an APIs for save, list and delete for a User entity and will explore the @SQLDelete, @Where, @Filter, and @FilterDef

@Entity
@Data
@Table(name = "user")
public class User {
@Id
@GeneratedValue
private Long id;
private String firstname;
private String lastname;
private String contact;
private Boolean deleted;
}
//APIs
@PostMapping(value = "/save")
public @ResponseBody User save(@RequestBody User user) {
user = userService.save(user);
return user;
}
@DeleteMapping("/delete/{id}")
public void delete(@PathVariable Long id) {
userService.delete(id);
}
@GetMapping(value = "/list")
public ResponseEntity<List<User>> findAll() {
List<User> users = userService.findAll();
return new ResponseEntity<>(users, HttpStatus.OK);
}

@SQLDelete

This annotation supports 3 parameter sql, callable and check.

  • 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.
ResultCheckStyle.NONE
Do not perform checking. Default Paramter
ResultCheckStyle.COUNT
Perform row-count checking. Row counts are the int values returned by both PreparedStatement.executeUpdate() and Statement.executeBatch(). These values are checked against some expected count.
ResultCheckStyle.PARAM
Essentially the same as COUNT except that the row count actually comes from an output parameter registered as part of a CallableStatement. This style explicitly prohibits statement batching from being usedimplmen

As we want to implement soft delete, we will update the deleted flag value to true and the record will be persisted in the database. To achieve this we have to add this statement in the User entity class

@Entity
@Data
@Table(name = "user")
@SQLDelete(sql = "UPDATE user SET deleted=true WHERE id=?")
public class User {
...
}

When we will make a delete request on “/delete/{id}” delete statement will be overridden by the SQL query defined in @SQLDelete. To verify, we can add Logging properties to use application.properties or application.yml. file.

#Logging properties
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Update statement generated by @SQLDelete

Now when we make a request to “/list” API to get the users, it returns all the users (deleted + not-deleted). To filter the users based on a deleted flag we can use JPA’s query keywords. Imagine a project with lots of methods where you need to implement soft delete, you have to modify all the methods with JPA keywords and might result in error-prone. We have @Where to rescue us from modifying all the methods and saves time. Add this annotation to the User entity as below.

@Entity
@Data
@Table(name = "user")
@SQLDelete(sql = "UPDATE user SET deleted=true WHERE id=?")
@Where(clause = "deleted = false")
public class User {
...
}
  • Without @Where(clause = “deleted = false”)
Response from “/list” API, all records are present.
  • With @Where(clause = “deleted = false”)
Response from “/list” API, only non-deleted users are present.

Note: You can not retrieve the records for users which has been removed as @Where removes the users' records with a deleted flag. If we have a requirement to show the records for both soft-deleted and not-deleted users we can use @Filter as explained in the below example.

Add the below statements in the User entity class and remove the @Where as keeping both the annotation will conflict the functionality of each other.

@Entity
@Data
@Table(name = "user")
@SQLDelete(sql = "UPDATE user SET deleted=true WHERE id=?")
@FilterDef(
name = "deletedUserFilter",
parameters = @ParamDef(name = "isDeleted", type = "boolean")
)
@Filter(
name = "deletedUserFilter",
condition = "deleted = :isDeleted"
)

public class User {
...
}

@FilterDef

This annotation defines the basic requirements which will be used by @Filter.

  • name: Name of the filter which will be used in @Filter and session
  • parameters: Defines the parameters using @ParamDef annotation with parameter name and type

@Filter

This uses the filter definition defined in @FilterDef using the name parameter. We can define our required condition using the condition parameter.

Let’s quickly modify the findAll() method. We will add the parameter named isDeleted to get the records of users based on it. To enable the filter we can add session.enableFilter(“filterName”) and can set the parameter value dynamically.

@GetMapping(value = "/list")
public ResponseEntity<List<User>> findAll(
@RequestParam(value = "isDeleted", required = false, defaultValue = "false") boolean isDeleted) {
List<User> users = userService.findAllFilter(isDeleted);
return new ResponseEntity<>(users, HttpStatus.OK);
}
public List<User> findAllFilter(boolean isDeleted) {
Session session = entityManager.unwrap(Session.class);
Filter filter = session.enableFilter("deletedUserFilter");
filter.setParameter("isDeleted", isDeleted);
List<User> users = userRepository.findAll();
session.disableFilter("deletedUserFilter");
return users;
}
  • Get records of soft-deleted users with parameter isDeleted=true
localhost:8080/users/rest/list?isDeleted=true
  • Get records of non-deleted users with parameter isDeleted=false
localhost:8080/users/rest/list?isDeleted=false

Get the source code from here.

--

--