Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

What does Auditing mean?

Auditing is intended to help us do Auditing. When we operate a record, we need to know who created it, when it was created, who was the last person to modify it, when it was last modified, and even modify the record. These are all supported by the Auditing in Spring Data JPA, which provides us with four annotations to do the above, as follows:

  • Which user created @createdby?
  • @createdDate Creation time.
  • LastModifiedBy last modifies the entity’s user.
  • @lastModifiedDate Time of last modification.

That is Auditing, so how is it actually implemented?

How is Auditing implemented?

There are three ways to implement Auditing using the four annotation implementations above, and let’s take a look at each.

The first method: add the above four annotations directly to the instance

We also use the previous example to add four fields to the User entity that record creator, creation time, last modified, and last modified.

Step one: in @ the Entity: four User to add annotations, and new @ EntityListeners annotation (AuditingEntityListener. Class).

Once added, the entity code for User is as follows:

@Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor @ToString(exclude = "addresses") @EntityListeners(AuditingEntityListener.class) public class User implements Serializable { @Id @GeneratedValue(strategy=  GenerationType.AUTO) private Long id; private String name; private String email; @Enumerated(EnumType.STRING) private SexEnum sex; private Integer age; @OneToMany(mappedBy = "user") @JsonIgnore private List<UserAddress> addresses; private Boolean deleted; @CreatedBy private Integer createUserId; @CreatedDate private Date createTime; @LastModifiedBy private Integer lastModifiedUserId; @LastModifiedDate private Date lastModifiedTime; }Copy the code

We need to do two things in the @Entity Entity:

1. The most important four fields record creator, creation time, last modified person and last modified time respectively. The code is as follows:

   @CreatedBy

   private Integer createUserId;

   @CreatedDate

   private Date createTime;

   @LastModifiedBy

   private Integer lastModifiedUserId;

   @LastModifiedDate

   private Date lastModifiedTime;
Copy the code

2. The AuditingEntityListener cannot be less and must pass this code:

@EntityListeners(AuditingEntityListener.class)
Copy the code

Annotate the Entity of the Entity.

Step 2: Implement the AuditorAware interface to tell JPA who the current user is.

We need to implement the AuditorAware interface, as well as the getCurrentAuditor method, and return an Integer user ID.

Public class MyAuditorAware implements AuditorAware<Integer> { Return the current user ID @Override public Optional<Integer> getCurrentAuditor() {ServletRequestAttributes ServletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); Integer userId = (Integer) servletRequestAttributes.getRequest().getSession().getAttribute("userId"); return Optional.ofNullable(userId); }}Copy the code

The key step here is to implement the AuditorAware interface as follows:

public interface AuditorAware<T> {

   T getCurrentAuditor();

}
Copy the code

It is important to note that there is more than one way to obtain the user ID. In practice, we may store the current user information in Session, Redis, or Spring security. In addition, the implementation here is slightly different. Let’s use Security as an example:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || ! authentication.isAuthenticated()) { return null; } Integer userId = ((LoginUserInfo) authentication.getPrincipal()).getUser().getId();Copy the code

The code to get the userId might look something like this, just so you know.

Step 3: Enable the Auditing function of JPA through the @enableJpaauditing annotation.

The third step is the most important, for the above configuration to take effect, we need to enable the Auditing feature of JPA (it is not enabled by default). The annotation needed here is @enablejpaauditing, which looks like this:

@Inherited @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(JpaAuditingRegistrar.class) Public @interface EnableJpaAuditing {//auditor user access method, default is to find AuditorAware implementation class; String auditorAwareRef() default ""; Boolean setDates() default true; Boolean setDates() default true; Boolean modifyOnCreate() default true; Boolean modifyOnCreate() default true; // The time generation method, the default is the current time. Since it is possible to expect the time to be constant when testing, it provides a custom method); String dateTimeProviderRef() default ""; }Copy the code

After learning about the @enableJpaauditing annotation, we need to create a Configuration file, add the @EnableJpaAuditing annotation, and load our MyAuditorAware into it, as follows:

@Configuration @EnableJpaAuditing public class JpaConfiguration { @Bean @ConditionalOnMissingBean(name = "myAuditorAware") MyAuditorAware myAuditorAware() { return new MyAuditorAware(); }}Copy the code

Rule of thumb:

  1. Here is a best practice for writing Congifuration. Why do we write a separate JpaConfiguration configuration file instead of putting @EnableJpaAuditing in the JpaApplication class? Because a JpaConfiguration file can be loaded and tested separately, wouldn’t it be necessary to start the entire application every time you put it in the Appplication class?
  2. MyAuditorAware can also be loaded with the @Component annotation. Why do I recommend the @Bean approach? This way, users can see which components we have customized directly from our configuration files without being unnecessarily surprised. This is a bit of framework writing experience for your reference.

Step 4: Let’s write a test case to test it.

@DataJpaTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Import(JpaConfiguration.class) public class UserRepositoryTest { @Autowired private UserRepository userRepository; @MockBean MyAuditorAware myAuditorAware; @test public void testAuditing() {// Since the Test case is not our focus, we mock out our methods using @mockBean, Looking forward to return 13 this user ID Mockito. When (myAuditorAware. GetCurrentAuditor ()). ThenReturn (Optional) of (13)); User User = user.builder ().name("jack").email("[email protected]").sex(SexEnum.BOY).sex(SexEnum.BOY).sex(SexEnum. .age(20) .build(); userRepository.save(user); // Verify that the creation time, update time, UserID is correct; List<User> users = userRepository.findAll(); Assertions.assertEquals(13,users.get(0).getCreateUserId()); Assertions.assertNotNull(users.get(0).getLastModifiedTime()); System.out.println(users.get(0)); }}Copy the code

Note that:

  1. We use @mockBean to simulate the UserID MyAuditorAware returns result 13;
  2. We test and verify that create_user_id is what we expect.

The test results are as follows:

User(id=1, name=jack, [email protected], sex=BOY, age=20, deleted=null, createUserId=13, createTime=Sat Oct 03 21:19:57 CST 2020, lastModifiedUserId=13, lastModifiedTime=Sat Oct 03 21:19:57 CST 2020)
Copy the code

The results were exactly as we expected.

Is it now possible to learn the first way of Auditing? In addition, Spring Data JPA provides a second way for entities to implement the Auditable interface directly.

The second method: Implement the Auditable interface in the entity

Let’s change the User entity object as follows:

@Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor @ToString(exclude = "addresses") @EntityListeners(AuditingEntityListener.class) public class User implements Auditable<Integer,Long, Instant> { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; private String name; private String email; @Enumerated(EnumType.STRING) private SexEnum sex; private Integer age; @OneToMany(mappedBy = "user") @JsonIgnore private List<UserAddress> addresses; private Boolean deleted; private Integer createUserId; private Instant createTime; private Integer lastModifiedUserId; private Instant lastModifiedTime; @Override public Optional<Integer> getCreatedBy() { return Optional.ofNullable(this.createUserId); } @Override public void setCreatedBy(Integer createdBy) { this.createUserId = createdBy; } @Override public Optional<Instant> getCreatedDate() { return Optional.ofNullable(this.createTime); } @Override public void setCreatedDate(Instant creationDate) { this.createTime = creationDate; } @Override public Optional<Integer> getLastModifiedBy() { return Optional.ofNullable(this.lastModifiedUserId); } @Override public void setLastModifiedBy(Integer lastModifiedBy) { this.lastModifiedUserId = lastModifiedBy; } @Override public void setLastModifiedDate(Instant lastModifiedDate) { this.lastModifiedTime = lastModifiedDate; } @Override public Optional<Instant> getLastModifiedDate() { return Optional.ofNullable(this.lastModifiedTime); } @Override public boolean isNew() { return id==null; }}Copy the code

The difference from the first approach is that the above four annotations are removed and the code becomes redundant and verbose to implement the Auditable method.

All else being the same, we run the test case again and find the same effect. Given the complexity of the code, I don’t recommend this approach. So let’s look at the third way.

Third way: use the @mappedsuperClass annotation

We mentioned this annotation in lesson 6 when we talked about object polymorphism, and it was primarily used to solve the problem of a common BaseEntity, which represents that each class that inherits from it is a separate table.

Let’s first look at the syntax for @mappedsuperclass.

There is nothing in the annotation, but it represents the abstract relationship, that is, the common field of all subclasses. So let’s look at an example.

Step 1: Create a BaseEntity with some public fields and annotations for the entity.

package com.example.jpa.example1.base;

import org.springframework.data.annotation.*;

import javax.persistence.MappedSuperclass;

import java.time.Instant;

@Data

@MappedSuperclass

@EntityListeners(AuditingEntityListener.class)

public class BaseEntity {

   @CreatedBy

   private Integer createUserId;

   @CreatedDate

   private Instant createTime;

   @LastModifiedBy

   private Integer lastModifiedUserId;

   @LastModifiedDate

   private Instant lastModifiedTime;

}
Copy the code

Note: need to use the above mentioned four inside the BaseEntity annotations, and add @ EntityListeners (AuditingEntityListener. Class), so all the subclasses don’t need to add.

Step 2: The entity directly inherits from BaseEntity.

Let’s modify the above User instance to inherit BaseEntity as follows:

@Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor @ToString(exclude = "addresses") public class User extends  BaseEntity { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; private String name; private String email; @Enumerated(EnumType.STRING) private SexEnum sex; private Integer age; @OneToMany(mappedBy = "user") @JsonIgnore private List<UserAddress> addresses; private Boolean deleted; }Copy the code

In this case, the User entity doesn’t need to worry too much, we can just focus on the logic we need, as follows:

  1. Remove the @ EntityListeners (AuditingEntityListener. Class);
  2. Remove public fields for @createdby, @CreatedDate, @lastModifiedby, and @lastModifiedDate annotations.

Then we run the above test case again and find the same effect.

This way, is my most recommended, but also the most used in practical work of a way. The obvious benefits are common, simple code, and less concern.