We have explored on how to implement auditing with Spring Data Jpa, Spring Data Enver, and Spring Data Mongo. In this tutorial, we will implement auditing with Spring Data JDBC.

Static Badge Gradle Plugin Portal Version GitHub License GitHub Actions Workflow Status GitHub Repo stars

Background

Spring Data JDBC provides the convenience to enable auditing for a class. This can be achieved by using the following annotations:

  • @EnableJdbcAuditing

  • @Created

  • @CreatedBy

  • @LastModified

  • @LastModifiedBy

Configuration

In order to enable auditing we will create a @Configuration class that is also annotated with @EnableJdbcAuditing and expose a @Bean of AuditorAware:

@Configuration
@EnableJdbcAuditing
class AuditConfiguration {

    @Bean
    public AuditorAware<String> auditorAware() {
        return () -> Optional.of("Mr. Auditor");
    }

}

AuditConfiguration demonstrates a simple implementation of AuditorAware which returns Mr. Auditor. This will be assigned to fields that are annotated with @CreatedBy and @LastModifiedBy.

Audited Class

Next, we will implement User which stores audit information:

@Table("users")
class User {

    @Id
    private Long id;

    @CreatedDate
    private Instant created;

    @CreatedBy
    private String createdBy;

    @LastModifiedDate
    private Instant lastModified;

    @LastModifiedBy
    private String lastModifiedBy;

    private final String name;
    private String username;

    User(String name, String username) {
        this.name = name;
        this.username = username;
    }

    public User username(String username) {
        this.username = username;
        return this;
    }

}

Values for the fields created, createdBy, lastModified, and lastModifiedBy will be handled by the framework.

Repository Class

Finally, we will implement the repository class - UserRepository which extends `CrudRepository class from Spring Data:

interface UserRepository extends CrudRepository<User, Long> {
}

Verification

As always, we will verify our implementation through database integration test. We will utilise @Testcontainers and @DataJdbcTest annotations.

Unlike Spring Data JPA / Hibernate, Spring Data JDBC does not support automatic creation of a table. Therefore, we will use @Sql in our test to create the table users before running our tests. The setup will look as follows:

@Testcontainers
@DataJdbcTest(includeFilters = @Filter(EnableJdbcAuditing.class))
@Sql(
        executionPhase = BEFORE_TEST_CLASS,
        statements = "CREATE TABLE users (id BIGSERIAL PRIMARY KEY, created TIMESTAMP WITH TIME ZONE NOT NULL, created_by TEXT NOT NULL, last_modified TIMESTAMP WITH TIME ZONE NOT NULL, last_modified_by TEXT NOT NULL, name TEXT NOT NULL, username TEXT NOT NULL)"
)
class UserAuditTests {

    @Container
    @ServiceConnection
    private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));

    @Autowired
    private UserRepository repository;

}

@DataJdbcTest is not aware about our AuditConfiguration class. For that, we are using includeFilters to inform it about the class.

Create new User

Upon the creation of a new User, the annotated fields should be automatically assigned. Whereby created and lastModified are assigned with current time while createdBy and lastModifiedBy are assigned with Mr. Auditor:

class UserAuditTests {

    @Autowired
    private UserRepository repository;

    @Test
    @DisplayName("When a user is persisted Then created and lastModified fields are set And createdBy and lastModifiedBy fields are set to Mr. Auditor")
    void create() {
        var user = repository.save(new User("Rashidi Zin", "rashidi"));

        assertThat(user).extracting("created", "lastModified").doesNotContainNull();
        assertThat(user).extracting("createdBy", "lastModifiedBy").containsOnly("Mr. Auditor");
    }

}

Update an existing User

When updating an existing User, the field lastModified should be updated. The following test demonstrates that there is a User created seven days ago and once updated, its lastModified field should be later than created field:

class UserAuditTests {

    @Autowired
    private UserRepository repository;

    @Test
    @DisplayName("Given there is a user When I update its username Then lastModified field should be updated")
    @Sql(statements = "INSERT INTO users (id, created, created_by, last_modified, last_modified_by, name, username) VALUES (84, CURRENT_TIMESTAMP - INTERVAL '7 days', 'Mr. Auditor', CURRENT_TIMESTAMP - INTERVAL '7 days', 'Mr. Auditor', 'Rashidi Zin', 'rashidi');")
    void update() {
        var modifiedUser = repository.findById(84L).map(user -> { user.username("rashidi.zin"); return user; }).map(repository::save).orElseThrow();

        var created = (Instant) ReflectionTestUtils.getField(modifiedUser, "created");
        var modified = (Instant) ReflectionTestUtils.getField(modifiedUser, "lastModified");

        assertThat(modified).isAfter(created);
    }

}

Full implementation of the test can be found in UserAuditTests.