February 28, 2022

Spring Data JPA Auditing Example

In this post we’ll see how to configure Spring Data JPA to automatically store auditing information like created by, created date, modified by, modified date for any entity.

Spring Data auditing support

Spring Data provides support to keep track of who created or changed an entity and the time when this happened. To use this audit functionality you need to equip your entity classes with auditing metadata that can be defined either using annotations or by implementing an interface.

Annotations that are used are as following-

  • @CreatedBy- To capture the user who created the entity.
  • @LastModifiedBy- To capture the user who modified the entity.
  • @CreatedDate- To capture the point in time when entity is created.
  • @LastModifiedDate- To capture the point in time when entity was last modified.

If you want to see how to create Maven project, please check this post- Create Java Project Using Maven in Eclipse

For setting up a Spring Data JPA project and the required maven dependencies please check this post- Spring Data JPA Example

Apart from the dependencies mentioned in that post you also need spring-aspects.jar on the classpath for auditing features, for that you need to add following dependency.

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>${spring.version}</version>
</dependency>

Spring Data annotations @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate

In the example we’ll see how to configure Spring Data JPA to automatically store the auditing information like created by, created date, modified by, modified date for any entity.

For the example entity used is post and the table has columns post_title and post_content to store title and content. Apart from that information about the user who is creating or modifying any record and the time of creation and modification is also persisted.

DB Table

MySQL DB table used for this Spring data JPA auditing example can be created using the following query.

CREATE TABLE `post` (
  `post_id` int(11) NOT NULL AUTO_INCREMENT,
  `post_title` varchar(100) NOT NULL,
  `post_content` varchar(250) DEFAULT NULL,
  `created_by` varchar(45) DEFAULT NULL,
  `created_date` datetime DEFAULT NULL,
  `modified_by` varchar(45) DEFAULT NULL,
  `modified_date` datetime DEFAULT NULL,
  PRIMARY KEY (`post_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

JPA Entity class

There should be an entity class which maps to the post table in DB. Better practice is to keep audit related fields in a separate class and the entities that need audit information can extend that class. That way other entities can also reuse the super class.

So there is a class Audit which is annotated as @MappedSuperclass. A mapped superclass has no separate table defined for it. It's mapping information is applied to the entities that inherit from it.

Spring Data JPA provides an entity listener called AuditingEntityListener that can be used to trigger capturing auditing information. AuditingEntityListener class has callback methods annotated with @PrePersist and @PreUpdate annotations that are triggered for persist event and update event respectively.

You can enable the AuditingEntityListener per entity using the @EntityListeners annotation. You can also specify your own custom listener class with @EntityListeners annotation.

import java.util.Date;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
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 Audit<T> {
  @CreatedBy
  @Column(name="created_by")
  protected T createdBy;
  @LastModifiedBy
  @Column(name="modified_by")
  protected T modifiedBy;
  @CreatedDate
  @Temporal(TemporalType.TIMESTAMP)
  @Column(name="created_date")
  protected Date createdDate;
  @LastModifiedDate
  @Temporal(TemporalType.TIMESTAMP)
  @Column(name="modified_date")
  protected Date modifiedDate;
  public T getCreatedBy() {
    return createdBy;
  }
  public void setCreatedBy(T createdBy) {
    this.createdBy = createdBy;
  }
  public T getModifiedBy() {
    return modifiedBy;
  }
  public void setModifiedBy(T modifiedBy) {
    this.modifiedBy = modifiedBy;
  }
  public Date getCreatedDate() {
    return createdDate;
  }
  public void setCreatedDate(Date createdDate) {
    this.createdDate = createdDate;
  }
  public Date getModifiedDate() {
    return modifiedDate;
  }
  public void setModifiedDate(Date modifiedDate) {
    this.modifiedDate = modifiedDate;
  }	
}

As you can see above super class has fields to capture creation and modification data which is mapped to appropriate columns.

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="post")
public class Post extends Audit<String> {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name="post_id")
  private int id;
  @Column(name="post_title")
  private String postTitle;
  @Column(name="post_content")
  private String content;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getPostTitle() {
    return postTitle;
  }
  public void setPostTitle(String postTitle) {
    this.postTitle = postTitle;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }

  @Override
  public String toString() {
    
    return "Id= "+ getId() + " Title= " + getPostTitle() + " Content= "+ getContent()
    + " createdBy= " + getCreatedBy() + " modifiedBy= " + getModifiedBy() 
    + " createdDate= " + getCreatedDate() + " modifiedDate= " + getModifiedDate() ;
  }
}

AuditorAware interface

In case you use either @CreatedBy or @LastModifiedBy annotations, the auditing infrastructure needs to capture information about the current user. To do so, you have to implement an AuditorAware<T> interface infrastructure to tell who the current user or system interacting with the application is.

For our example we are taking the user who is currently logged in using the system property.

import java.util.Optional;
import org.springframework.data.domain.AuditorAware;

public class LoggedInUserAuditorAwareImpl implements AuditorAware<String>{

  @Override
  public Optional<String> getCurrentAuditor() {
    return Optional.of(System.getProperty("user.name"));
  }
}

Spring Data JPA Repository

import org.springframework.data.repository.CrudRepository;
import com.knpcode.springproject.model.Post;

public interface PostRepository extends CrudRepository<Post, Integer>{

}

You can see that PostRepository interface extends CrudRepository which takes the domain class to manage (Post in this case) as well as the id type of the domain class as type arguments.

Service class

From the service layer we’ll call the repository methods. Notice that repository instance has to be injected in the service class.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.knpcode.springproject.dao.PostRepository;
import com.knpcode.springproject.model.Post;

@Service
public class PostService {
  @Autowired
  private PostRepository repository;
  public Post addPost(Post post) {
    return repository.save(post);
  }

  public Post getPostById(int id) {
    return repository.findById(id).get();
  }
}

Java Config class

In this Spring data JPA auditing example Java configuration is used so class is annotated with @Configuration annotation.

@ComponentScan annotation configures component scanning.

@EnableJpaRepositories annotation enables the JPA repositories. Package to scan for the repositories is provided as a value with this annotation.

@EnableTransactionManagement annotation enables Spring's annotation-driven transaction management capability.

@EnableJpaAuditing annotation enables auditing in JPA.

For setting up DataSource DB properties are read from a properties file, path for the properties file is configured using @PropertySource annotation.

With in this Java config class we set up a EntityManagerFactory and use Hibernate as persistence provider. There is also a bean definition for AuditorAware implementation.

@Configuration
@ComponentScan(basePackages = "com.knpcode.springproject")
@EnableJpaRepositories(basePackages = "com.knpcode.springproject.dao")
@EnableTransactionManagement
@EnableJpaAuditing
@PropertySource("classpath:config/db.properties")
public class JPAConfig {
  @Autowired
  private Environment env;
  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setPackagesToScan("com.knpcode.springproject.model");
    factory.setDataSource(dataSource());
    factory.setJpaProperties(hibernateProperties());
    return factory;
  }

  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName(env.getProperty("db.driverClassName"));
    ds.setUrl(env.getProperty("db.url"));
    ds.setUsername(env.getProperty("db.username"));
    ds.setPassword(env.getProperty("db.password"));
    return ds;
  }

  Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.setProperty("hibernate.dialect", env.getProperty("hibernate.sqldialect"));
    properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.showsql"));
    return properties;
  }

  @Bean
  public PlatformTransactionManager transactionManager() {
    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory().getObject());
    return txManager;
  }

  @Bean
  public AuditorAware<String> auditorProvider() {
    return new LoggedInUserAuditorAwareImpl();
  }
}
db.properties file
db.driverClassName=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/knpcode
db.username=
db.password=
hibernate.sqldialect=org.hibernate.dialect.MySQLDialect
hibernate.showsql=true

Testing the example

Following class with main method is used to test the example. Initially a post instance is persisted. As you can see from the query audit information (created_by, created_date, modified_by, modified_date) is also inserted.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import com.knpcode.springproject.model.Post;
import com.knpcode.springproject.service.PostService;

public class App {
  public static void main(String[] args) {
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(JPAConfig.class);
    PostService postService =  context.getBean("postService", PostService.class);
    Post post = new Post();
    post.setPostTitle("A Post on Spring Data JPA");
    post.setContent("Spring Data JPA reduces boiler plate code");
    postService.addPost(post);
    //postService.updatePost(1);
    
    context.close();
  }
}

Generated query from the logs-

Hibernate: insert into post (created_by, created_date, modified_by, modified_date, post_content, post_title) values (?, ?, ?, ?, ?, ?)

Fetching record

Fetching the record that is persisted.

public class App {
  public static void main(String[] args) {
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(JPAConfig.class);
    PostService postService =  context.getBean("postService", PostService.class);

    Post post = postService.getPostById(1);
    System.out.println("Post- "+ post);
    context.close();
  }
}

Generated Query and fetched record

Hibernate: select post0_.post_id as post_id1_1_0_, post0_.created_by as created_2_1_0_, post0_.created_date as created_3_1_0_, post0_.modified_by as modified4_1_0_, post0_.modified_date as modified5_1_0_, post0_.post_content as post_con6_1_0_, post0_.post_title as post_tit7_1_0_ from post post0_ where post0_.post_id=?

Post- Id= 1 Title= A Post on Spring Data JPA Content= Spring Data JPA reduces boiler plate code createdBy= knpcode modifiedBy= knpcode createdDate= 2019-09-30 11:16:47.0 modifiedDate= 2019-09-30 11:16:47.0
Updating the record
public class App {
  public static void main(String[] args) {
    AbstractApplicationContext context = new AnnotationConfigApplicationContext(JPAConfig.class);
    PostService postService =  context.getBean("postService", PostService.class);
    
    Post post = postService.getPostById(1);
    post.setPostTitle("New Title");
    postService.addPost(post);
    post = postService.getPostById(1);
    System.out.println("Post- " + post);
    context.close();
  }
}

As you can see modified date is changed when the record is fetched now.

Post- Id= 1 Title= New Title Content= Spring Data JPA reduces boiler plate code createdBy= knpcode modifiedBy= knpcode createdDate= 2019-09-30 11:16:47.0 modifiedDate= 2019-09-30 12:08:50.0

That's all for the topic Spring Data JPA Auditing Example. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment