Cloud

Auditing With Spring Data JPA

Auditing with Hibernate Envers is a small thing to implement but is the easiest way to audit persistent data in a Spring Boot application. However, Envers is opinionated and may not meet data auditing requirements for your organization, such as audit table schema design or content.

Spring Boot applications that require more flexible auditing capabilities can implement auditing using Spring Data JPA.

Auditing approach

This approach implements callbacks for JPA lifecycle events. The sample implementation abstracts common code and common audit data to suit applications that work with many entities:

  • A listener class for each JPA entity encapsulates callback methods for each CUD operation on the entity.
  • Each listener class extends a common abstract listener class.
  • Each entity registers its listener class via a Spring Data JPA annotation.

The Abstract Listener Class

Code common for entity listener callbacks is provided by an abstract listener class.

abstract class AbstractEntityListener<E> {




  void doCreate(E entity) {

    postProcess(entity, “CREATE”);

  }




  void doUpdate(E entity) {

    postProcess(entity, ”UPDATE”);

  }




  void doDelete(E entity) {

    postProcess(entity, “DELETE”);

  }




  private void postProcess(E entity, String action) {

    TransactionSynchronizationManager.registerSynchronization(

      new TransactionSynchronizationAdapter() {

        @Override

        public void afterCompletion(int status) {

          if (status == STATUS_COMMITTED) {

            persist(entity, action);

          }

        }

      });

  }




  private void persist(E entity, String action) {

    EntityManagerFactory entityManagerFactory =

      AuditUtil.getBean(EntityManagerFactory.class);

    EntityManager entityManager = entityManagerFactory.createEntityManager();

    AuditBase auditEntity = mapAuditEntity(entity);

    auditEntity.setAuditAction(action);

    auditEntity.setAuditDate(Instant.now());




    entityManager.getTransaction().begin();

    entityManager.persist(auditEntity);

    entityManager.flush();

    entityManager.getTransaction().commit();

    entityManager.close();

  }




  abstract AuditBase mapAuditEntity(E entity);




}

NOTES:

  1. The doCreate, doUpdate, and doDelete provide default implementations for lifecycle callbacks on each of the CUD operations on the entity.
  2. The postProcess method uses Spring transaction synchronization for fine-grained control on when entity auditing occurs. In this example, the audit data is persisted only after the change to the corresponding entity is actually committed.
  3. The persist method does the actual work of persisting the audit data by creating an EntityManager object and using it to do the work.
  4. Each audit entity extends AuditBase, which provides common audit fields.
  5. Entity listeners are instantiated by JPA and therefore Spring dependency injection is not supported within an entity listener class. The AuditUtil class provides the mechanism for obtaining Spring beans within entity listeners.

The AuditUtil Class

Covid 19
COVID-19: Digital Insights For Enterprise Action

Access Perficient’s latest insights into how you can leverage digital technologies to not only respond to the pandemic, but drive your operations forward and deliver experiences your customers need.

Get Informed

This class implements ApplicationContextAware and provides method getBean(), which returns a Spring bean of the requested class.

@Component

public class AuditUtil implements ApplicationContextAware {

  private static ApplicationContext context;




  @Override

  public void setApplicationContext(ApplicationContext applicationContext) {

    setContext(applicationContext);

  }




  public static<T> T getBean(Class<T> beanClass) {

    return context.getBean(beanClass);

  }




  private static void setContext(ApplicationContext context) {

    AuditUtil.context = context;

  }




}

The Entity Listener Class

The listener class for each entity provides the actual JPA callbacks.

public class MyEntityListener extends AbstractEntityListener<MyEntity> {




  @PostPersist

  public void onCreate(MyEntity entity) {

    // Custom updates on the modified entity, if any.

    doCreate(entity);

  }




  @PostUpdate

  public void onUpdate(MyEntity entity) {

    // Custom updates on the modified entity, if any.

    doUpdate(entity);

  }




  @PostRemove

  public void onDelete(MyEntity entity) {

    // Custom updates on the modified entity, if any.

    doDelete(entity);

  }




  @Override

  public MyEntityAudit mapAuditEntity(MyEntity entity) {

    // Populate the audit entity from data in the modified entity.

  }




}

NOTES:

  1. The annotation on each CUD method registers the lifecycle callback with JPA.
  2. The mapAuditEntity method provides the code that copies data from the entity to its corresponding audit entity. Mappers such as mapstuct or modelMapper are good choices when the audit data closely matches the entity data.

The Entity Class

Each entity class is written using normal JPA annotations but also registers its entity listener class with JPA.

@Entity

@Table(name = “MY_ENTITY”)

@EntityListeners(MyEntityListener.class)

Public class MyEntity {

  …

}

The Entity Audit Class

An entity audit class is written using normal JPA annotations and the audit data can be customized in any way that satisfies your organization’s data auditing requirements. In this sample implementation, each entity audit class extends AuditBase, which provides common audit data.

@Entity

@Table(name = “MY_ENTITY_AUDIT”)

public class MyEntityAudit extends AuditBase {

   …

}

The AuditBase Class

AuditBase provides common audit data.

@MappedSuperclass

public abstract class AuditBase {

  @Column(name = “AUDIT_ACTION”)

  private String auditAction;




  @Column(name = “AUDITED_AT”)

  private Instant auditedAt;




  @Column(name = “CREATED_BY”)

  private String createdBy;




  @Column(name = “CREATED_AT”)

  private Instant createdAt;




  @Column(name = “UPDATED_BY”)

  private String updatedBy;




  @Column(name = “UPDATED_AT”)

  private Instant updatedAt;

}

References:

https://www.baeldung.com/database-auditing-jpa

https://dev.to/njnareshjoshi/jpa-auditing-persisting-audit-logs-automatically-using-entitylisteners-238p

About the Author

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up