Automating Data Auditing with MyBatis Plus Field Population

In modern application development, maintaining audit trails for database records is a common requirement. This typically involves fields such as creation timestamps, last update timestamps, and the identifiers of users responsible for these actions. Manually populating these fields in every data manipulation operation is prone to errors, creates repetitive code, and can lead to inconsistencies. MyBatis Plus, a powerful enhancement to the MyBatis framework, offers an elegant solution for automatic field population, streamlining this process significantly.

Implementing Automatic Field Population

The core mechanism for this feature in MyBatis Plus involves two primary components: specific annotations on your entity class fields and a custom hendler that intercepts database operations.

1. Annotating Entity Fields

To enable automatic population for a field, you apply the @TableField annotation with its fill attribute. The FieldFill enumeration dictates when the field should be populated: on insertion, on update, or both.

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class ProductEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    private String productName;
    private String productCode;
    private Double price;
    private Integer stockQuantity;

    // Field to be filled only on insertion
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime creationTimestamp;

    // Field to be filled on both insertion and update
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime lastUpdateTimestamp;

    // Field to be filled only on insertion
    @TableField(fill = FieldFill.INSERT)
    private Long createdByUserId;

    // Field to be filled on both insertion and update
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long lastUpdatedByUserId;
}

2. Creating a Metadata Object Handler

The second step involves implementing the MetaObjectHandler interface provided by MyBatis Plus. This handler acts as an interceptor for your entity's metadata during save and update operations. You override the insertFill and updateFill methods to define the logic for populating your audit fields.

Within these methods, you use metaObject.setValue("fieldName", value) (or the more convenient setFieldValByName) to dynamically assign values to the fields marked with @TableField(fill = ...).

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class GlobalAuditFieldHandler implements MetaObjectHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalAuditFieldHandler.class);

    /**
     * Executes logic to fill fields during an INSERT operation.
     * @param metaObject The metadata object containing entity details.
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        logger.debug("Executing insertFill for audit fields.");
        // Check if the entity has a setter for creationTimestamp before attempting to set it
        if (metaObject.hasSetter("creationTimestamp")) {
            this.setFieldValByName("creationTimestamp", LocalDateTime.now(), metaObject);
        }
        // Often, the last update timestamp is also initialized on insert
        if (metaObject.hasSetter("lastUpdateTimestamp")) {
            this.setFieldValByName("lastUpdateTimestamp", LocalDateTime.now(), metaObject);
        }
        if (metaObject.hasSetter("createdByUserId")) {
            // Retrieve current user ID from a thread-local context
            this.setFieldValByName("createdByUserId", SecurityContextHolder.getCurrentPrincipalId(), metaObject);
        }
        if (metaObject.hasSetter("lastUpdatedByUserId")) {
            // Retrieve current user ID from a thread-local context
            this.setFieldValByName("lastUpdatedByUserId", SecurityContextHolder.getCurrentPrincipalId(), metaObject);
        }
    }

    /**
     * Executes logic to fill fields during an UPDATE operation.
     * @param metaObject The metadata object containing entity details.
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        logger.debug("Executing updateFill for audit fields.");
        if (metaObject.hasSetter("lastUpdateTimestamp")) {
            this.setFieldValByName("lastUpdateTimestamp", LocalDateTime.now(), metaObject);
        }
        if (metaObject.hasSetter("lastUpdatedByUserId")) {
            // Retrieve current user ID from a thread-local context
            this.setFieldValByName("lastUpdatedByUserId", SecurityContextHolder.getCurrentPrincipalId(), metaObject);
        }
    }
}

Managing Current User Identity with ThreadLocal

For fields like createdByUserId and lastUpdatedByUserId, the GlobalAuditFieldHandler needs access to the ID of the user currently logged in or performing the action. Direct access to HTTP session or request objects is not feasible within a MyBatis Plus handler. This is where Java's ThreadLocal mechanism becomes essential.

What is ThreadLocal?

ThreadLocal provides a way to store data that is local to a specific thread. Each thread that accesses a ThreadLocal variable gets its own independent copy of that variable. This ensures that changes made by one thread to its variable copy do not interfere with the copies held by other threads, offering thread isolation.

In the context of a web application, each client request is typically handled by a dedicated thread from a thread pool. This allows you to set a user's ID in a ThreadLocal at the beginning of a request (e.g., in an authentication filter or interceptor). Any code executed by that same thread during the request's lifecycle, including your GlobalAuditFieldHandler, can then retrieve this ID. It's crucial to clear the ThreadLocal variable at the end of the request to prevent memory leaks and ensure data integrity for subsequent requests handled by the same thread.

Implementing a Security Context Holder

A static utility class can encapsulate the ThreadLocal management for the current user's ID:

public class SecurityContextHolder {

    private static final ThreadLocal<Long> currentPrincipalId = new ThreadLocal<>();

    /**
     * Sets the ID of the currently authenticated principal for the current thread.
     * This should typically be called at the start of a request, e.g., by a filter.
     * @param userId The ID of the principal (user).
     */
    public static void setCurrentPrincipalId(Long userId) {
        currentPrincipalId.set(userId);
    }

    /**
     * Retrieves the ID of the currently authenticated principal from the current thread's context.
     * @return The principal's ID, or null if no principal is set for the current thread.
     */
    public static Long getCurrentPrincipalId() {
        return currentPrincipalId.get();
    }

    /**
     * Clears the principal ID from the current thread's context.
     * This method MUST be called at the end of the request processing to prevent
     * memory leaks and ensure thread isolation.
     */
    public static void clearCurrentPrincipalId() {
        currentPrincipalId.remove();
    }
}

By integrating SecurityContextHolder.getCurrentPrincipalId() into your GlobalAuditFieldHandler, you can seamlessly populate user-related audit fields based on the context of the executing thread.

Tags: MyBatis Plus Automatic Field Population Data Auditing ThreadLocal java

Posted on Sun, 09 Aug 2026 16:31:10 +0000 by cliffboss