Auditing with Spring Boot 2 Spring Data JPA MySQL Example - RameshMF/spring-boot-developers-guide GitHub Wiki
In this article, we will discuss how can we configure JPA to automatically persist the CreatedBy, CreatedDate, LastModifiedBy, and LastModifiedDate columns for any entity. We will create a simple Spring Boot CRUD REST APIs and we implement auditing using spring data JPA.
In any business application, auditing simply means tracking and logging every change we do in our persisted records, which simply means tracking every insert, update, and delete operation and storing it. Basically, auditing helps us in maintaining history records, which can later help us in tracking user activities.
Instead of writing code manually on each save, update or delete operation why not we use third party library to do automatically for us. Spring Data provides sophisticated support to transparently keep track of who created or changed an entity and the point in time this happened. To benefit from that functionality you have to equip your entity classes with auditing metadata that can be defined either using annotations or by implementing an interface.
In this example, we will create a common generic Auditable abstract class with audit fields so that any entity can extends it to use auditing.
If you are auditing multiple entities then it is common practice to extract common fields with abstract class and extend it. So instead of creating createdBy, createdDate, lastModifiedBy, and lastModifiedDate properties in each entity, we can move the createdBy, createdDate, lastModifiedBy, lastModifiedDate properties to a base class, Auditable, and annotate this base class with @MappedSuperClass. Later, we can use the Auditable class in other audited entities.
- What we’ll build
- Tools and Technologies Used
- Creating and Importing a Project
- Packaging Structure
- The pom.xml File
- Configuring MySQL Database
- Create Generic Auditable Class with Spring Data Annotations @CreatedBy, @CreatedDate, @LastModifiedBy, and @LastModifiedDate
- Create a JPA Entity which extends Auditable Class - User.java
- Auditing Author Using AuditorAware and Spring Security
- Enable JPA Auditing by Using @EnableJpaAuditing 10 Create Spring Data JPA Repository - UserRepository.java
- Creating UserController(Contains REST APIs)
- Running the Application
- Test Using Postman Client
We are building a simple Spring boot CRUD Rest APIs to demonstrate the usage of Spring Data JPA auditing with MySQL as a database.
- Spring Boot - 2.0.4.RELEASE
- JDK - 1.8 or later
- Spring Framework - 5.0.8 RELEASE
- Hibernate - 5.2.17.Final
- Maven - 3.2+
- Spring Data JPA - 2.0.10 RELEASE
- IDE - Eclipse or Spring Tool Suite (STS)
- MYSQL - 5.1.47
There are many ways to create a Spring Boot application. The simplest way is to use Spring Initializr at http://start.spring.io/, which is an online Spring Boot application generator.
Look at the above diagram, we have specified following details:
- Generate: Maven Project
- Java Version: 1.8 (Default)
- Spring Boot:2.0.5
- Group: net.guides.springboot
- Artifact: springboot2-jpa-auditing
- Name: springboot2-jpa-auditing
- Package Name : net.guides.springboot.springboot2jpaauditing
- Packaging: jar (This is the default value)
- Dependencies: Web, JPA, MySQL, DevTools
Once, all the details are entered, click on Generate Project button will generate a spring boot project and downloads it. Next, Unzip the downloaded zip file and import it into your favorite IDE.
Following is the project structure for your reference -
diagram here
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.guides.springboot</groupId>
<artifactId>springboot2-jpa-auditing</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot2-jpa-auditing</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The Spring Boot Maven plugin provides many convenient features:
-
It collects all the jars on the classpath and builds a single, runnable "über-jar", which makes it more convenient to execute and transport your service.
-
It searches for the public static void main() method to flag as a runnable class.
-
It provides a built-in dependency resolver that sets the version number to match Spring Boot dependencies. You can override any version you wish, but it will default to Boot’s chosen set of versions.
Configure application.properties to connect to your MySQL database. Let's open application.properties file and add following database configuration to it.
spring.datasource.url = jdbc:mysql://localhost:3306/users_database?useSSL=false
spring.datasource.username = root
spring.datasource.password = root
## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
Change the above configuration such as JDBC URL, username and password as per your environment.
7. Create Generic Auditable Class with Spring Data Annotations @CreatedBy, @CreatedDate, @LastModifiedBy, and @LastModifiedDate
If you are auditing multiple entities then it is common practice to extract common fields with abstract class and extend it. So instead of creating createdBy, createdDate, lastModifiedBy, and lastModifiedDate properties in each entity, we can move the createdBy, createdDate, lastModifiedBy, lastModifiedDate properties to a base class, Auditable, and annotate this base class with @MappedSuperClass. Later, we can use the Auditable class in other audited entities.
package net.guides.springboot.springboot2jpaauditing.audit;
import static javax.persistence.TemporalType.TIMESTAMP;
import java.util.Date;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.Temporal;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable<U> {
@CreatedBy
protected U createdBy;
@CreatedDate
@Temporal(TIMESTAMP)
protected Date createdDate;
@LastModifiedBy
protected U lastModifiedBy;
@LastModifiedDate
@Temporal(TIMESTAMP)
protected Date lastModifiedDate;
public U getCreatedBy() {
return createdBy;
}
public void setCreatedBy(U createdBy) {
this.createdBy = createdBy;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public U getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(U lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Date getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
package net.guides.springboot.springboot2jpaauditing.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import net.guides.springboot.springboot2jpaauditing.audit.Auditable;
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User extends Auditable<String> {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(name = "email_address", nullable = false)
private String emailId;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmailId() {
return emailId;
}
public void setEmailId(String emailId) {
this.emailId = emailId;
}
}
Let's understand important JPA Auditing annotations:
-
@CreatedDate - Declares a field as the one representing the date the entity containing the field was created.
-
@LastModifiedDate - Declares a field as the one representing the date the entity containing the field was recently modified.
-
@CreatedBy- Declares a field as the one representing the principal that created the entity containing the field.
-
@LastModifiedBy - Declares a field as the one representing the principal that recently modified the entity containing the field.
The Spring Data JPA approach abstracts working with JPA callbacks and provides us these fancy annotations to automatically save and update auditing entities.
Spring Data JPA provides a JPA entity listener class, AuditingEntityListener, which contains the callback methods (annotated with the @PrePersist and @PreUpdate annotations), which will be used to persist and update these properties when we will persist or update our entity.
JPA provides the @EntityListeners annotation to specify callback listener classes, which we use to register our AuditingEntityListener class.
However, we can also define our own callback listener classes if we want to and specify them using the @EntityListeners annotation. In my next article, I will demonstrate how we can use @EntityListeners to store audit logs.
JPA can analyze createdDate and lastModifiedDate using current system time, but what about the createdBy and lastModifiedBy fields? How will JPA recognize what to store in them?
To tell JPA about currently logged-in users, we will need to provide an implementation of AuditorAware and override the getCurrentAuditor() method. And inside getCurrentAuditor(), we will need to fetch a currently logged-in user.
As of now, I have provided a hard-coded user, but if you are using Spring Security, then use it to find the currently logged-in user.
package net.guides.springboot.springboot2jpaauditing.audit;
import java.util.Optional;
import org.springframework.data.domain.AuditorAware;
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("Ramesh");
// Use below commented code when will use Spring Security.
}
}
// ------------------ Use below code for spring security --------------------------
/*class SpringSecurityAuditorAware implements AuditorAware<User> {
public User getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
return ((MyUserDetails) authentication.getPrincipal()).getUser();
}
}*/
Now, we want to enable JPA auditing by specifying @EnableJpaAuditing on one of our configuration classes, in this example, I have specified @EnableJpaAuditing on main Springboot2JpaAuditingApplication class. @EnableJpaAuditing accepts one argument, auditorAwareRef, where we need to pass the name of the AuditorAware bean.
package net.guides.springboot.springboot2jpaauditing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import net.guides.springboot.springboot2jpaauditing.audit.AuditorAwareImpl;
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class Springboot2JpaAuditingApplication {
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAwareImpl();
}
public static void main(String[] args) {
SpringApplication.run(Springboot2JpaAuditingApplication.class, args);
}
}
Now, we completed all JPA auditing setup so let's see other files as well and then we will see demo of it.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.companyname.springbootcrudrest.model.User;
@Repository
public interface UserRepository extends JpaRepository<User, Long>{
}
Note that, we have annotated the interface with @Repository annotation. This tells Spring to bootstrap the repository during a component scan.
Now, it's time to create CRUD Rest APIs for User model.
package net.guides.springboot.springboot2jpaauditing.controller;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import net.guides.springboot.springboot2jpaauditing.exception.ResourceNotFoundException;
import net.guides.springboot.springboot2jpaauditing.model.User;
import net.guides.springboot.springboot2jpaauditing.repository.UserRepository;
@RestController
@RequestMapping("/api/v1")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(
@PathVariable(value = "id") Long userId) throws ResourceNotFoundException {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found :: " + userId));
return ResponseEntity.ok().body(user);
}
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) {
return userRepository.save(user);
}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(
@PathVariable(value = "id") Long userId,
@Valid @RequestBody User userDetails) throws ResourceNotFoundException {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found :: " + userId));
user.setEmailId(userDetails.getEmailId());
user.setLastName(userDetails.getLastName());
user.setFirstName(userDetails.getFirstName());
user.setLastModifiedDate(new Date());
final User updatedUser = userRepository.save(user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/users/{id}")
public Map<String, Boolean> deleteUser(
@PathVariable(value = "id") Long userId) throws ResourceNotFoundException {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found :: " + userId));
userRepository.delete(user);
Map<String, Boolean> response = new HashMap<>();
response.put("deleted", Boolean.TRUE);
return response;
}
}
Let's understand all the annotations used in the UserController
- @RequestMapping("/api/v1") - annotation declares that the url for all the apis in this controller will start with /api/v1
- @RestController - annotation is a combination of Spring’s @Controller and @ResponseBody annotations.
- @GetMapping("/users") - annotation is a short form of @RequestMapping(value="/users", method=RequestMethod.GET).
- @GetMapping("/users/{id}") - annotation is a short form of @RequestMapping(value="/users/{id}", method=RequestMethod.GET).
- @PostMapping("/users") - annotation is a short form of @RequestMapping(value="/users", method=RequestMethod.POST).
- @PutMapping("/users/{id}") - annotation is a short form of @RequestMapping(value="/users/{id}", method=RequestMethod.PUT).
- @DeleteMapping("/user/{id}") - annotation is a short form of @RequestMapping(value="/users/{id}", method=RequestMethod.DELETE).
- @PathVariable - annotation is used to bind a path variable with a method parameter.
From your IDE, run the Springboot2JpaAuditingApplication.main() method as a standalone Java class that will start the embedded Tomcat server on port 8080 and point the browser to http://localhost:8080/. 13. Test Using Postman Client