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.