In the dynamic world of software development, understanding the complete history of your data is crucial. Who made a change? When did it occur? Who viewed the data? Spring JPA Auditing, combined with custom solutions, offers a comprehensive way to answer these questions, acting as a time machine for your database records.

What is JPA Auditing?

The Java Persistence API (JPA) is the standard for object-relational mapping in Java. While JPA itself doesn’t have a built-in auditing mechanism, Spring Data JPA, a powerful extension, seamlessly fills this gap. JPA Auditing allows you to automatically track:

  • Creation Information: Who created a record and when.
  • Modification Information: Who last updated a record and when.
  • Accessed Information: Who accessed what data.

The beauty of this is that it’s all done behind the scenes, requiring minimal developer intervention.

Why Audit Your Data?

Auditing isn’t just about compliance or security, though those are crucial aspects. Consider these benefits:

  • Troubleshooting: Quickly pinpoint the origin of data errors.
  • Data Recovery: Roll back to previous states in case of accidental modifications.
  • Transparency: Understand who’s interacting with your data and how.
  • Regulatory Compliance: Fulfill requirements for data retention and accountability.
  • Security and Anomaly Detection: Identify unusual access patterns.

Spring JPA Auditing in Action

Let’s dive into how you can harness the power of Spring JPA Auditing:

Enable Auditing:

In your Spring Boot application’s main configuration class, add the @EnableJpaAuditing annotation:

@Configuration
@EnableJpaAuditing
public class AppConfig {
    // ... your other configurations
}

Annotate Your Entities:

Use the following annotations in your entity classes to mark fields for auditing:

  • @CreatedBy: Captures the creator’s username or ID.
  • @CreatedDate: Records the creation timestamp.
  • @LastModifiedBy: Tracks the last modifier’s username or ID.
  • @LastModifiedDate: Stores the last modification timestamp.
@Entity
public class Product {
    // ... other fields

    @CreatedBy
    private String createdBy;

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedBy
    private String lastModifiedBy;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

AuditorAware (Optional):

To customize how you capture the current user (e.g., from Spring Security), implement the AuditorAware interface:

@Component
public class AuditorAwareImpl implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        // ... your logic to fetch the current user's username or ID
    }
}

Storing Audit Information in a Separate Table with EntityListeners

While Spring JPA Auditing conveniently stores audit information directly within the audited entities, a separate audit log table offers more flexibility and better reporting capabilities.

Create an Audit Log Entity:

Design an entity to hold the audit log entries:

@Entity
public class AuditLog {
    @Id
    @GeneratedValue
    private Long id;

    private String entityType; // e.g., "Product"
    private Long entityId;
    private String action;     // e.g., "CREATE", "UPDATE", "DELETE", "READ"
    private String modifiedBy; 
    private LocalDateTime modifiedDate;
    // ... other relevant fields (old values, new values)
}

Implement an EntityListener:

Create an EntityListener that intercepts entity lifecycle events and saves audit log entries:

public class AuditLogListener {
    @PostPersist
    public void postPersist(Object entity) {
        saveAuditLog(entity, "CREATE");
    }

    @PostUpdate
    public void postUpdate(Object entity) {
        saveAuditLog(entity, "UPDATE");
    }

    @PostRemove
    public void postRemove(Object entity) {
        saveAuditLog(entity, "DELETE");
    }

    // ... (saveAuditLog method implementation)
}

Register the EntityListener:

Annotate the entities you want to audit with @EntityListeners:

@Entity
@EntityListeners(AuditLogListener.class)
public class Product { /* ... */ }

Example: Audit Log Table

CREATE TABLE audit_log (
    id BIGSERIAL PRIMARY KEY,
    entity_type VARCHAR(255) NOT NULL,
    entity_id BIGINT NOT NULL,
    action VARCHAR(255) NOT NULL,
    user_id VARCHAR(255) NOT NULL,
    timestamp TIMESTAMP NOT NULL
);

Tracking Data Access (Read Events) with an Audit Table

To track when data is accessed (read), a common approach is to use an audit table in combination with aspect-oriented programming (AOP):

Create a ReadLog Entity:

Design an entity to hold read access information:

@Entity
public class ReadLog {
    @Id
    @GeneratedValue
    private Long id;

    private String entityType; // e.g., "Product"
    private Long entityId;
    private String accessedBy;
    private LocalDateTime accessedDate;
}

Implement an Aspect:

Use AOP to intercept service or repository method calls that retrieve data and save an entry in the ReadLog table:

@Aspect
@Component
public class ReadLogAspect {
    @Autowired
    private ReadLogRepository readLogRepository;

    @AfterReturning(pointcut = "execution(* com.example.repository.*.find*(..))", returning = "result")
    public void logDataAccess(JoinPoint joinPoint, Object result) {
        if (result != null) {
            // Extract entity information from the method arguments or result
            String entityType = ...; // Get the entity type
            Long entityId = ...;    // Get the entity ID

            ReadLog readLog = new ReadLog();
            readLog.setEntityType(entityType);
            readLog.setEntityId(entityId);
            readLog.setAccessedBy(getCurrentAuditor());
            readLog.setAccessedDate(LocalDateTime.now());

            readLogRepository.save(readLog);
        }
    }

    // ... (Implementation of getCurrentAuditor similar to AuditorAware)
}

Soft Deletes with JPA Auditing

Soft deletes are a way to mark a record as deleted without actually removing it from your database. This is particularly useful for data recovery and maintaining historical records. Here’s how to integrate soft deletes with JPA auditing:

Add a Deleted Flag:

Include a boolean field in your entity to indicate whether a record is soft deleted:

@Entity
public class Product {
    // ... other fields

    private boolean deleted = false;  // Default to not deleted
}

Modify Repository Methods:

Instead of using delete(), create custom repository methods that update the deleted flag and trigger auditing:

public interface ProductRepository extends JpaRepository<Product, Long> {
    @Modifying
    @Query("UPDATE Product p SET p.deleted = true WHERE p.id = :id")
    void softDelete(@Param("id") Long id);
}

Querying:

Adjust your queries to filter out soft-deleted records by default:

List<Product> findProductsByDeletedFalse(); // Find non-deleted products

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.