Auditing Created and Updated Times

Fill createdAt, updatedAt and createdBy automatically with Spring Data auditing, and know which writes bypass it.

Syntax@EnableJpaAuditing @Configuration class JpaAuditingConfig {} @EntityListeners(AuditingEntityListener.class) @MappedSuperclass abstract class Auditable { @CreatedDate Instant createdAt; @LastModifiedDate Instant updatedAt; }

Almost every table wants to know when a row was created and last changed, and often by whom. Setting those fields by hand in each service is easy to forget. Spring Data JPA's auditing sets them on every persist and update.

Three pieces

  1. @EnableJpaAuditing on a @Configuration class turns the feature on.
  2. @EntityListeners(AuditingEntityListener.class) on the entity, or on a shared @MappedSuperclass, hooks into the JPA lifecycle.
  3. Fields marked @CreatedDate, @LastModifiedDate, @CreatedBy and @LastModifiedBy from org.springframework.data.annotation.

Use Instant for the timestamps and map them to timestamp with time zone columns. LocalDateTime has no zone, so a server moved between regions, or a summer time change, makes the values ambiguous. Mark the created column @Column(updatable = false) so an update can never overwrite it.

By default the last-modified field is also set when the entity is created, so a new row has both timestamps equal. That makes "changed since" queries simple. Set modifyOnCreate = false on @EnableJpaAuditing if you want it left null until the first update.

Who made the change

@CreatedBy and @LastModifiedBy need an AuditorAware bean that returns the current user, usually the name from Spring Security's SecurityContextHolder. Return Optional.empty() for scheduled jobs and other unauthenticated work, or a fixed value such as system so the column can stay not null.

What bypasses it

  • Bulk @Modifying queries and JdbcClient statements never touch entities, so no listener runs. Set the columns in the SQL.
  • Saving an entity that has no changes does not update its timestamp, because Hibernate issues no UPDATE.
  • Changes to a child collection alone do not mark the parent as modified.

Where to put @EnableJpaAuditing

Put it on its own configuration class, not on the main application class. Annotations on the main class apply to every test slice, and @WebMvcTest has no JPA set up, so it fails to start with a metamodel error.

Hibernate's own @CreationTimestamp and @UpdateTimestamp fill timestamps without Spring Data and are fine for that alone. Spring Data's version adds the user fields and lets you supply the clock, which makes timestamps testable.

Example

Example · java
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
class JpaAuditingConfig { }

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
abstract class Auditable {
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private Instant updatedAt;

    public Instant getCreatedAt() { return createdAt; }
    public Instant getUpdatedAt() { return updatedAt; }
}

@Entity
public class Article extends Auditable {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    protected Article() { }

    public Article(String title) { this.title = title; }

    public void retitle(String title) { this.title = title; }
}

When to use it

  • Show when each support ticket was opened and last changed without setting timestamps in every service method.
  • Record which administrator last edited a product for an internal history screen.
  • Write tests that assert exact createdAt values by fixing the application clock.

More examples

Recording the user

This class replaces the plain JpaAuditingConfig: enable auditing once per application, because a second @EnableJpaAuditing registers a duplicate jpaAuditingHandler bean and startup fails. Entities with @CreatedBy String createdBy and @LastModifiedBy String updatedBy now get the login name. Anonymous and background work is recorded as system rather than left null. The SecurityContext is per thread, so work handed to another executor needs the context propagated or it will be audited as system too.

Example · java
import java.util.Optional;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

@Configuration
@EnableJpaAuditing(auditorAwareRef = "currentUser")
public class AuditingConfig {

    @Bean
    AuditorAware<String> currentUser() {
        return () -> {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) {
                return Optional.of("system");
            }
            return Optional.of(auth.getName());
        };
    }
}

A clock you control in tests

Auditing reads the time from the DateTimeProvider, which here reads the same Clock bean the rest of the application uses. A test can replace the Clock with Clock.fixed(...) and assert exact createdAt values instead of checking that a timestamp is roughly now.

Example · java
import java.time.Clock;
import java.util.Optional;
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;

@Configuration
@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider")
public class ClockedAuditingConfig {

    @Bean
    Clock clock() {
        return Clock.systemUTC();
    }

    @Bean
    DateTimeProvider auditingDateTimeProvider(Clock clock) {
        return () -> Optional.of(clock.instant());
    }
}

Keeping timestamps right in bulk updates

The bulk update never passes through AuditingEntityListener, so the statement sets updatedAt itself. Pass the time in from the caller's Clock rather than using the database's current_timestamp, so the value matches what auditing would have written and tests stay deterministic.

Example · java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.time.Instant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

@Entity
class Subscription {
    @Id
    private Long id;
    private String status;
    private Instant expiresAt;
    private Instant updatedAt;

    protected Subscription() { }
}

public interface SubscriptionRepository extends JpaRepository<Subscription, Long> {

    @Transactional
    @Modifying(clearAutomatically = true)
    @Query("""
        update Subscription s
           set s.status = 'EXPIRED', s.updatedAt = :now
         where s.status = 'ACTIVE' and s.expiresAt < :now
        """)
    int expireDue(@Param("now") Instant now);
}

Discussion

  • Be the first to comment on this lesson.