When Simple Audit is Enough
Auditing can mean many different things; sometimes it means saving every state change that occurs in your system on a remote server, and other times it means keeping a history of every state in your database.
In this blog, we will concentrate on a simple use case; We just want to keep track of who and when a record has been created and updated, without recording the previous state.
Database Auditing
Database auditing means keeping track of state changes in you data. In our use case, we want to know who created or changed an entity and when they did so. We are not interested to know the change that occurred. This would require more sophisticated auditing patterns or frameworks.
I have seen projects storing this data manually, which can be problematic. It forces you to write boilerplate code, which implies less maintainability. It obscures the business logic and mixes two orthogonal concerns, the business and technical logic. It is also error prone as you can easily forget to update the 'updateDate' when you are concentrating on the business aspect of your code.
A better approach is to let your frameworks take care of the technical aspects of tracking auditing values. Spring Data JPA provides a convenient way to keep track of who created an aggregate and when it has been changed.
Spring Data
Spring data provides four useful annotations.
- @CreatedBy
- @LastModifiedBy
- @CreatedDate
- @LastModifiedDate
In the following I will guide you through the steps to add simple auditing capability to your Spring Data JPA application.
Simple Approach
We will consider a dummy aggregate called 'Item', which has only one business attribute: value. In its simplest form, the class would look something like that.
The Aggregate
class Item {
String value;
}
As we want to save it to a database with JPA and audit the changes, we have to add a few fields. The full class looks then like this:
@EntityListeners(AuditingEntityListener.class)
@Entity
class Item {
@Id
String id;
@CreatedBy
String createdBy;
@LastModifiedBy
String lastModifiedBy;
@CreatedDate
ZonedDateTime createdDate;
@LastModifiedDate
ZonedDateTime lastModifiedDate;
String value;
}
'id' is the primary key and probably straightforward to the reader. 'createdBy' and 'lastModifiedBy' insert the current user. Obviously the current user has to be provided. The 'createDate' and 'lastModifiedDate' will be inserted before the commit. We can choose from Java Time, long, Long, legacy Date and even Joda-Time for the type. 'EntityListeners(AuditingE...)' enables the auditing for the aggregate.
Current User
In order to capture the 'createdBy' and 'lastModifiedBy', we need a bean to provides the user information. For that we need to register a bean of type 'AuditorAware'. Note that the user does not need to be a string, any type will do. We typically extract the user from the security context.
@Configuration
@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider")
class DbConfig {
@Bean
AuditorAware<String> auditor() {
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of(SecurityContextHolder.getContext().getAuthentication().getName());
}
};
}
}
Bootstrapping the Auditing
@EnableJpaAuditing is the key here. It makes the auditing infrastructure pick up the 'AuditorAware' bean and fill the auditing fields before the commit to the database.
The catch:
Spring doesn't convert to Java times gracefully. So, we will have to register a dateTimeProviderRef. The implementation is trivial.
@Component("dateTimeProvider")
class DefaultDateTimeProvider implements DateTimeProvider {
public Optional<TemporalAccessor> getNow() {
return Optional.of(ZonedDateTime.now());
}
}
It Works
You can find the complete code at the following GitHub repository. It is a minimalistic application which saves and reads items via rest calls.
It works but the solution above can be improved.
The Hierarchy Approach
The 'Item' class has 5 technical fields for 1 business field. This makes the class harder to read One approach to solve the issue is to put the audit field in a super class.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
class Audit {
@CreatedBy
String createdBy;
@LastModifiedBy
String lastModifiedBy;
@CreatedDate
ZonedDateTime createdDate;
@LastModifiedDate
ZonedDateTime lastModifiedDate;
}
The super class provides the required attributes for auditing. If we need to audit a class, just inherit from Audit and we are done.
@Entity
class HierarchyItem extends Audit {
@Id
String id;
String value;
}
The Composition Approach
The class hierarchy approach has its own challenges. Composition is generally favored over inheritance. This can be done as well.
@AccessType(AccessType.Type.FIELD)
@Embeddable
class Audit {
@CreatedBy
String createdBy;
@LastModifiedBy
String lastModifiedBy;
@CreatedDate
ZonedDateTime createdDate;
@LastModifiedDate
ZonedDateTime lastModifiedDate;
}
and the aggregate
@EntityListeners(AuditingEntityListener.class)
@Entity
public class CompositionItem {
@Id
String id = UUID.randomUUID().toString();
@Embedded
Audit audit = new Audit();
String value;
}
Composition is not supported by all versions of Spring Data JPA. It is confirmed to work with version 2.2 and confirmed not to work for version 1.11.
Conclusion
In the blog entry, I showed you how to implement a simple auditing solution with Spring Data JPA's help. It is not a full blown solution for tracking every change in you database. For that other tools or patterns should be considered. Never the less, many times, just tracking who and when the last state change occurred is enough. If you implement the composition, the hierarchy or the plain solution depends on the specific problem you have to solve. The plain option will typically be used in a legacy application, where you can't change all you aggregates at once. The hierarchy approach can be used if you know all your JPA entities will need a common behaviour. The composition is more flexible in that it allows, for example, to separate a business hierarchy from technical details.
A minimalistic project can be found on GitHub. It demonstrates auditing features for all three strategies.