Spring Boot JPA Auditing

0

Audit fields determine who created or changed an entity and when the change happened. Since auditing is required for most of the entities, we will create a base entity class which contains just the audit fields createdBy, createdDate,lastModifiedBye and lastModifiedDate so whichever entity needs to be auditable will extend AditableEntity.

In order to let the framework know about the audit fields, we will have to annotate the fields with @CreatedBy and @LastModifiedBy to capture the user who created or modified the entity as well as @CreatedDate and @LastModifiedDate to capture when the change happened.

We also need to configure a JPA entity listener to trigger the capturing of auditing information on persiting and updating entities.
We specify the callback listener class AuditingEntityListener to set on a per-entity basis by using the @EntityListeners annotation.

AditableEntity:

package com.javarticles.springboot.movies.domain;

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.AbstractPersistable;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.ManyToOne;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners({AuditingEntityListener.class})
public abstract class AditableEntity<PK extends Serializable> extends AbstractPersistable<PK> {

    private @CreatedDate LocalDateTime createdDate;
    private @LastModifiedDate LocalDateTime lastModifiedDate;

    private @ManyToOne @CreatedBy User createdBy;
    private @ManyToOne @LastModifiedBy User lastModifiedBy;

    public LocalDateTime getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(LocalDateTime createdDate) {
        this.createdDate = createdDate;
    }

    public LocalDateTime getLastModifiedDate() {
        return lastModifiedDate;
    }

    public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }

    public User getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(User createdBy) {
        this.createdBy = createdBy;
    }

    public User getLastModifiedBy() {
        return lastModifiedBy;
    }

    public void setLastModifiedBy(User lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }
}

Any entity that wants to be auditable will extend AditableEntity.

Movie:

package com.javarticles.springboot.movies.domain;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;

@Entity
public class Movie extends AditableEntity<Long> {
    private String name;
    
    private Integer year;
    
    @Enumerated(EnumType.STRING)
    private Genre genre;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getYear() {
        return year;
    }

    public void setYear(Integer year) {
        this.year = year;
    }

    public Genre getGenre() {
        return genre;
    }

    public void setGenre(Genre genre) {
        this.genre = genre;
    }
}

Genre:

package com.javarticles.springboot.movies.domain;

public enum Genre {
COMEDY,
SCIFI,
ACTION,
ROMANCE,
THRILLER,
HORROR,
MYSTERY,
CRIME,
ANIMATION,
ADVENTURE,
FANTASY,
DRAMA,
SUPERHERO
}

We define a simple User entity to represent the ‘who’ part in the creation and modification.

User:

package com.javarticles.springboot.movies.domain;

import javax.persistence.Entity;

@Entity
public class User extends AditableEntity<Long> {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The machinery that updates the ‘createdBy’ or ‘modifiedBy’ needs to needs to somehow become aware of the user making the change. In order to do so, spring relies on the bean that implements AuditorAware interface to return the current user.

Below is a simple implementation of it. In a more real implementation we should be accessing the spring security provided Authentication object to look up the user.

MovieAuditorAware:

package com.javarticles.springboot.movies;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;


import org.springframework.data.domain.AuditorAware;

import com.javarticles.springboot.movies.domain.User;
import com.javarticles.springboot.movies.repositories.UserRepository;

public class MovieAuditorAware implements AuditorAware<User> {
    @Autowired
    private UserRepository userRepository;

    @Override
    public Optional<User> getCurrentAuditor() {
        return Optional.ofNullable(userRepository.findByName("admin"));
    }
}

Finally we need to enable the JPA auditing using @EnableJpaAuditing annotation and expose a bean of type AuditorAware
so spring can use it to figure out the user.

SpringbootMoviesJpaApplication:

package com.javarticles.springboot.movies;

import com.javarticles.springboot.movies.domain.User;
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;

@SpringBootApplication
@EnableJpaAuditing
public class SpringbootMoviesJpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootMoviesJpaApplication.class, args);
    }

    @Bean
    public AuditorAware<User> auditorProvider() {
        return new MovieAuditorAware();
    }
}

We need a movie and user repository to find the entities by name. We will use this in our test case.

MovieRepository:

package com.javarticles.springboot.movies.repositories;

import org.springframework.data.jpa.repository.JpaRepository;

import com.javarticles.springboot.movies.domain.Movie;

public interface MovieRepository extends JpaRepository<Movie, Long> {
    Movie findByName(String name);
}

UserRepository:

package com.javarticles.springboot.movies.repositories;

import com.javarticles.springboot.movies.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByName(String name);
}

In our test we create a user and  then create a movie. We then verify the audit properties.

SpringbootMoviesJpaApplicationTests:

package com.javarticles.springboot.movies.springbootMoviesJpa;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertNotNull;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.javarticles.springboot.movies.domain.Genre;
import com.javarticles.springboot.movies.domain.Movie;
import com.javarticles.springboot.movies.domain.User;
import com.javarticles.springboot.movies.repositories.MovieRepository;
import com.javarticles.springboot.movies.repositories.UserRepository;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootMoviesJpaApplicationTests {
    @Autowired
    private MovieRepository movieRepository;

    @Autowired
    private UserRepository userRepository;
    
	@Test
    public void createMovie() {
        User user = new User();
        user.setName("admin");
        userRepository.save(user);

        String name = "Murder on the Orient Express";
        Movie movie = new Movie();
        movie.setName(name);
        movie.setYear(2001);
        movie.setGenre(Genre.DRAMA);
        movieRepository.save(movie);
	    
        Movie movieFound = movieRepository.findByName(name);
        assertThat(movieFound, equalTo(movie));
        assertThat(movieFound.getGenre(), equalTo(movie.getGenre()));
        assertThat(movieFound.getCreatedBy(), equalTo(user));
        assertNotNull(movieFound.getCreatedDate()); 
    }

}

Data:

mysql> select * from movie;
+----+---------------------+---------------------+-------+------------------------------+------+---------------+---------------------+
| id | created_date        | last_modified_date  | genre | name                         | year | created_by_id | last_modified_by_id |
+----+---------------------+---------------------+-------+------------------------------+------+---------------+---------------------+
|  2 | 2018-06-25 09:52:33 | 2018-06-25 09:52:33 | DRAMA | Murder on the Orient Express | 2001 |             1 |                   1 |
+----+---------------------+---------------------+-------+------------------------------+------+---------------+---------------------+

Download the source code

This was an example about auditing using JPA.

You can download the source code here: springbootJpaAudit.zip
Share.

Comments are closed.