When we hear the word auditing, the first thing that comes to mind is an audit log that contains each version of the audited entity. Implementing an audit log is a complex task that takes a lot of time. Luckily, most of the time we don’t need to do it.
However, it is quite common that we must be able to answer to the following questions:
- When the entity X was created and/or modified?
- Who created and/or modified the entity X?
The auditing infrastructure of Spring Data JPA helps us to answer to these questions. This blog post describes how we can add the creation and modification time fields into our entities and update them by using the auditing infrastructure of Spring Data JPA.
Let’s start by creating a service that returns the current date and time.
If you are not familiar with Spring Data JPA, you should read the following blog posts before you continue reading this blog post:
- Spring Data JPA Tutorial: Introduction provides a quick introduction to Spring Data JPA and gives an overview of the Spring Data repository interfaces.
- Spring Data JPA Tutorial: Getting the Required Dependencies describes how you can get the required dependencies.
- Spring Data JPA Tutorial: Configuration describes how you can configure the persistence layer of a Spring application that uses Spring Data JPA.
Getting Current Date and Time
There are two reasons why we should create an interface that can be used for obtaining the current date and time. These reasons are:
- We want to create two different implementations for this interface:
- The first implementation is used by our application and it returns the current date and time.
- The second implementation is used by our integration tests and it returns always the same date and time.
- If we are implementing a real-life application, the odds are that our other components need to get the current date and time as well.
The DateTimeService interface declares only one method:
- The getCurrentDateAndTime() method returns a ZonedDateTime object.
The source code of the DateTimeService interface looks as follows:
import java.time.ZonedDateTime; public interface DateTimeService { ZonedDateTime getCurrentDateAndTime(); }
The CurrentTimeDateTimeService class implements the DateTimeService interface. Its getCurrentDateAndTime() method simply returns the current date and time.
The source code of the CurrentTimeDateTimeService looks as follows:
import java.time.ZonedDateTime; public class CurrentTimeDateTimeService implements DateTimeService { @Override public ZonedDateTime getCurrentDateAndTime() { return ZonedDateTime.now(); } }
Let’s move on and find out how we can integrate our service with the auditing infrastructure of Spring Data JPA.
Integrating Our Service With the Auditing Infrastructure of Spring Data JPA
The auditing infrastructure of Spring Data JPA uses the DateTimeProvider interface when it needs to get the current date and time. This means that if we want to integrate our DateTimeService with Spring Data JPA, we need to implement that interface. We can do this by following these steps:
- Create an AuditingDateTimeProvider class and implement the DateTimeProvider interface.
- Add a final DateTimeService field to the created class and inject it by using constructor injection.
- Implement the getNow() method. We need to fetch the current date and time by using the DateTimeService object and return a new GregorianCalendar object.
The source code of the AuditingDateTimeProvider class looks as follows:
import org.springframework.data.auditing.DateTimeProvider; import java.util.Calendar; import java.util.GregorianCalendar; public class AuditingDateTimeProvider implements DateTimeProvider { private final DateTimeService dateTimeService; public AuditingDateTimeProvider(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } @Override public Calendar getNow() { return GregorianCalendar.from(dateTimeService.getCurrentDateAndTime()); } }
Our next step is to configure the application context of our application. Let’s find out how we can do it.
Configuring the Application Context
First, we need to create a DateTimeService bean that is used when we run our application. We should declare this bean in the root application context configuration class (or XML configuration file) because it is probably used by more than one component, and I think that the root application context configuration class (or XML configuration file) is a natural place for this kind of beans.
We can create this bean by following these steps:
- Create the currentTimeDateTimeService() method and implement it by returning a new CurrentTimeDateTimeService object.
- Annotate the method with the @Bean annotation.
- Annotate the method with the @Profile annotation and set its value to Profiles.APPLICATION. This ensures that this bean is created only when our application is started.
The relevant part of the ExampleApplicationContext class looks as follows:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; @Configuration @ComponentScan("net.petrikainulainen.springdata.jpa") @Import({WebMvcContext.class, PersistenceContext.class}) public class ExampleApplicationContext { @Profile(Profiles.APPLICATION) @Bean DateTimeService currentTimeDateTimeService() { return new CurrentTimeDateTimeService(); } }
Additional Reading:
Second, we need to create the DateTimeProvider bean and enable the auditing support of Spring Data. We can do this by making the following changes to the configuration class that configures the persistence layer of our example application:
- Create a dateTimeProvider() method that returns a DateTimeProvider object and takes a DateTimeService object as a method parameter.
- Implement the method by creating a new AuditingAwareDateTimeProvider object.
- Annotate the created method with the @Bean annotation.
- Annotate the configuration class with the @EnableJpaAuditing annotation and set the name of the DateTimeProvider bean (dateTimeProvider) as the value of of its dataTimeProviderRef attribute.
The relevant part of the PersistenceContext class looks as follows:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider") @EnableJpaRepositories(basePackages = { "net.petrikainulainen.springdata.jpa.todo" }) @EnableTransactionManagement class PersistenceContext { @Bean DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) { return new AuditingDateTimeProvider(dateTimeService); } }
Let's move on and make the required changes to our entity class.
Modifying Our Entity Class
We need to make the following changes to our entity class (Todo):
- We need to ensure that the value of the creationTime field is set when our entity is persisted for the first time.
- We need to ensure that the value of the modificationTime field is set when our entity is persisted for the first time and updated when the information of our entity is updated.
We can make these changes by following these steps:
- Annotate the creationTime field with the @CreatedDate annotation. This identifies the field whose value is set when the entity is persisted to the database for the first time.
- Annotate the modificationTime field with the @LastModifiedDate annotation. This identifies the field whose value is set when the entity is persisted for the first time and updated when the information of the entity is updated.
- Annotate the entity class with the @EntityListeners annotation and set its value to AuditingEntityListener.class. The AuditingEntityListener class is a JPA entity listener that updates the audit information of an entity when it is persisted and updated.
The relevant part of the Todo class looks as follows:
import org.hibernate.annotations.Type; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; 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 javax.persistence.Version; import java.time.ZonedDateTime; @Entity @EntityListeners(AuditingEntityListener.class) @Table(name = "todos") final class Todo { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(name = "creation_time", nullable = false) @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") @CreatedDate private ZonedDateTime creationTime; @Column(name = "description", length = 500) private String description; @Column(name = "modification_time") @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") @LastModifiedDate private ZonedDateTime modificationTime; @Column(name = "title", nullable = false, length = 100) private String title; @Version private long version; }
Typically it is a good idea to add the audit fields into an abstract base class and annotate it with the @EntityListener annotation. The reason why I didn’t do it here is that our example application has only one entity, and I wanted to keep things as simple as possible.
If we would move this information to the abstract base class, its source code would look as follows:
import org.hibernate.annotations.Type; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperClass import java.time.ZonedDateTime; @EntityListeners(AuditingEntityListener.class) @MappedSuperClass public abstract class BaseEntity { @Column(name = "creation_time", nullable = false) @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") @CreatedDate private ZonedDateTime creationTime; @Column(name = "modification_time") @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") @LastModifiedDate private ZonedDateTime modificationTime; }
Additional Reading:
- The Javadoc of the Auditable interface
- The Javadoc of the AbstractAuditable class
- The Javadoc of the AuditingEntityListener class
- The Javadoc of the @CreatedDate annotation
- The Javadoc of the @EntityListeners annotation
- The Javadoc of the @LastModifiedDate annotation
- The Javadoc of the @MapperSuperClass annotation
Let’s find out why we should use the auditing support of Spring Data JPA instead of the callback methods specified in the Java Persistence API.
Why Should We Use the Auditing Support of Spring Data JPA?
If we need to add the creation and modification time fields into our entities, we don’t have to use Spring Data JPA. We can set the field values of these fields by creating callback methods that are attached to the entity lifecycle events.
The source of an abstract base class, which uses this method, looks as follows:
import org.hibernate.annotations.Type; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.MappedSuperClass; import javax.persistence.PrePersist; import javax.persistence.PreUpdate; import java.time.ZonedDateTime; @MappedSuperClass public abstract class BaseEntity { @Column(name = "creation_time", nullable = false) @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") private ZonedDateTime creationTime; @Column(name = "modification_time") @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime") private ZonedDateTime modificationTime; @PrePersist public void prePersist() { ZonedDateTime now = ZonedDateTime.now(); this.creationTime = now; this.modificationTime = now; } @PreUpdate public void preUpdate() { this.modificationTime = ZonedDateTime.now(); } }
This method is a lot simpler than the solution that uses the auditing infrastructure of Spring Data JPA. The obvious question is:
Does it make any sense to use a more complex solution?
It depends. There are two reasons why it makes sense:
- If we have to write tests which ensure that the creation and modification times are correct, we have to use the auditing infrastructure of Spring Data JPA because it gives us the possibility to use a DateTimeProvider that always returns the same date and time.
- If we need to save the information of the user who created and/or modified an entity, we should use Spring Data for setting the creation and modification times as well. It just doesn’t make any sense to set the audit information of an entity by using two different mechanisms.
Let’s move on and summarize what we learned from this blog post.
Summary
This blog post has taught us three things:
- We can create our own date and time provider by implementing the DateTimeProvider interface. This is useful because it gives us the possibility to use a different provider for testing purposes.
- We can identify the creation and modification time fields by using annotations, set the values by implementing the Auditable interface, or extend the AbstractAuditable class.
- It is simpler to set the values of the creation and modification time fields by using entity lifecycle events and callback methods, but there are situations where we should use the auditing infrastructure of Spring Data JPA (even though it is a more complex solution).
The next part of this tutorial describes how we can get the answer to the question: who created and/or modified the entity X.
P.S. You can get the example application of this blog post from Github.
Nice, thanks.
You are welcome.
Hi Petri
I use JDK7. Don't have the ZoneDateTime interface.Is there any other method to use Spring Data Jpa auditing?
thanks
Hi Ronnie,
You can use the "old"
Date
objects as well. Just replaceZonedDateTime
withDate
and you are good to go.Hi Petri:
It's work, thanks again!
Hi Ronnie,
You are welcome!
Hi Petri,
Thanks for you tutorial. Why did you name the first bean after the implementation and the second after the interface?
Hi Hannes,
I assume that the first bean means the
CurrentTimeDateTimeService
class and the second bean means theAuditingDataTimeProvider
class.The actual application has two beans that implement the
DateTimeService
interface:CurrentTimeDateTimeService
class is used when the application context is loaded in an application mode (the Spring profileapplication
is active).ConstantTimeDateTimeService
class is used when the application context is loaded in an integration test mode (the Spring profileintegrationtest
is active).Because I wanted to separate these two classes, I gave them names that describe the implementation.
The name of the
AuditingDataTimeProvider
is not optimal (IMO). I gave it this name because I wanted to describe that it is used by the auditing infrastructure of Spring Data JPA (and I couldn't figure out a better name for it).Petri,
Per usual, great article!
I did notice however that when I attempt to implement your solution, I run into a bit of trouble with the AuditingDateTimeProvider.class.
The method is missing a return type but it's implicitly 'void' so no biggie there. But then it doesn't allow me to set the field as final. Attempting to auto wire the service also gives me some runtime issues.
Any thoughts?
Hi Connor,
Which method are you talking about? The reason why I ask that the only "method" of the
AuditingDataTimeProvider
class that doesn't have a return type is the constructor. This also explains why cannot mark the field asfinal
. Do you have a typo in the class name or in the constructor name?Just saved my day!! thanks a ton!!!
Best regards from Brazil!!!
Hi, i feel the auditing provided by Hibernate envers is more exhaustive where one can track the revisions done to data, The example explains a basic audit of created by created date, modified by and modified date, does the spring jpa data auditing ensure revisions are maintained in the audit table
Hi,
You are right. If you need "real" auditing that tracks revisions, you should use Hibernate Envers (or some other similar tool).
poor maming convention and confusing
Hi,
Could you clarify your feedback? At the moment it's not very useful because I have no idea what is the target of your criticism (Spring Data JPA or my example code). In any case, I hope to hear from you because it's always interesting to learn from my readers.
Can i ask how his approach could be adapted if we want to save the audit entries to a separate audit table that captures the id of the entry, along with dates and old/new values of modified object?
Seems that Hibernate Envers is a more suitable choice for this
Hi,
You are correct. If you need to create a "real" audit log, Hibernate Envers is a good choice.