This is the seventh day of my participation in the First Challenge 2022

scenario

In the development of back-end services, SSM (SpringBoot + Spring + MyBatis) is a very popular framework combination. When we develop some business systems, there will be a lot of business data tables, and the information in the tables will be newly inserted, and many operations may be carried out in the whole life cycle.

For example, when we buy a product on a website, an order record will be generated. After the payment, the order status will change to paid. When we finally receive the order, the order status will change to completed.

Suppose our order table T_order results as follows:

Set insert_BY, insert_time, update_BY, and update_time when the order is created.

When updating the order status, you only need to update update_BY and update_time values.

So how do you deal with that?

The muggle way

The easiest thing to do, and the easiest to think of, is to process the relevant fields in the code for each business process.

For example, the order creation method is handled as follows:

public void create(Order order){
    / /... Other code
    // Set the audit field
    Date now = new Date();
    order.setInsertBy(appContext.getUser());
    order.setUpdateBy(appContext.getUser());
    order.setInsertTime(now);
    order.setUpdateTime(now);
    orderDao.insert(order);
}
Copy the code

The order update method only sets updateBy and updateTime

public void update(Order order){
    / /... Other code

    // Set the audit field
    Date now = new Date();
    order.setUpdateBy(appContext.getUser());
    order.setUpdateTime(now);
    orderDao.insert(order);
}
Copy the code

This approach can complete the function, but there are some problems:

  • You need to decide which fields to set in each method according to different business logic;
  • As the number of business models increases, there are Settings in the business methods of each model and too much repetitive code.

So once we know there’s a problem with this approach, we need to find out if there’s a good one. Read on!

Elegant approach

Since our persistence framework uses MyBatis more, we rely on MyBatis interceptors for our functionality.

First of all, what is an interceptor?

What is an interceptor?

MyBatis interceptor, as the name suggests, intercepts certain operations. Interceptors allow you to intercept some methods before and after execution, adding some processing logic.

MyBatis interceptors can be on the Executor, StatementHandler, PameterHandler and ResultSetHandler interface to intercept, that is to say, to the four objects for agent.

The original intention of interceptor design is to let users in the process of MyBatis do not have to modify the source code of MyBatis, can be integrated into the whole execution process in the way of plug-in.

For example, MyBatis has BatchExecutor, ReuseExecutor, SimpleExecutor, and CachingExecutor. If none of these query methods can meet your requirements, MyBatis (MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis, MyBatis

Interceptors in MyBatis are represented by the Interceptor interface, which has three methods.

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}
Copy the code

The plugin method is used by the interceptor to encapsulate the target object and can return either the target object itself or a proxy for it.

We can call the Intercept method when a proxy is returned, and we can call other methods as well.

The setProperties method is used to specify properties in the Mybatis configuration file.

Update audit fields with interceptors

So how do we implement our audit field assignment via interceptors?

When we create and modify orders, we essentially execute insert and update statements through MyBatis, which is handled through Executor.

We can intercept executors with an interceptor, and then set insert_BY,insert_time, update_BY,update_time, and other values for the data objects to be inserted based on the statement executed.

Custom interceptors

The most important thing for customizing Interceptor is to implement the plugin method and intercept method.

In the plugin method we can decide whether to intercept and thus what target object to return.

An intercept method is a method that is executed when an intercept is intended.

As for the plugin method, Mybatis already provides an implementation for us. Mybatis has a Plugin class with a static method called wrap(Object target,Interceptor Interceptor) that determines whether the Object to be returned is the target Object or the corresponding proxy.

However, there is a problem here. How do we know in the interceptor that the table to be inserted has audit fields to work with?

Because not all of our tables are business tables, there may be dictionary tables or definition tables that do not have audit fields, which we do not need to deal with in the interceptor.

That is, we need to be able to distinguish which objects need to update the audit field.

Here we can define an interface that is implemented by all models that need to update audit fields, and this interface acts as a marker.

public interface BaseDO {}public class Order implements BaseDO{

    private Long orderId;

    private String orderNo;

    private Integer orderStatus;

    private String insertBy;

    private String updateBy;

    private Date insertTime;

    private Date updateTime;
    / /... getter ,setter
}
Copy the code

Next, we can implement our custom interceptor.

@Component("ibatisAuditDataInterceptor")
@Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})})
public class IbatisAuditDataInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(IbatisAuditDataInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // Get the user name from the context
        String userName = AppContext.getUser();
        
        Object[] args = invocation.getArgs();
        SqlCommandType sqlCommandType = null;
        
        for (Object object : args) {
            // Get the operation type from the MappedStatement parameter
            if (object instanceof MappedStatement) {
                MappedStatement ms = (MappedStatement) object;
                sqlCommandType = ms.getSqlCommandType();
                logger.debug("Operation type: {}", sqlCommandType);
                continue;
            }
            // Check whether the parameter is BaseDO
            // A parameter
            if (object instanceof BaseDO) {
                if (SqlCommandType.INSERT == sqlCommandType) {
                    Date insertTime = new Date();
                    BeanUtils.setProperty(object, "insertedBy", userName);
                    BeanUtils.setProperty(object, "insertTimestamp", insertTime);
                    BeanUtils.setProperty(object, "updatedBy", userName);
                    BeanUtils.setProperty(object, "updateTimestamp", insertTime);
                    continue;
                }
                if (SqlCommandType.UPDATE == sqlCommandType) {
                    Date updateTime = new Date();
                    BeanUtils.setProperty(object, "updatedBy", userName);
                    BeanUtils.setProperty(object, "updateTimestamp", updateTime);
                    continue; }}// Compatible with MyBatis update Byexampleselinnovation (record, example);
            if (object instanceof ParamMap) {
                logger.debug("mybatis arg: {}", object);
                @SuppressWarnings("unchecked")
                ParamMap<Object> parasMap = (ParamMap<Object>) object;
                String key = "record";
                if(! parasMap.containsKey(key)) {continue;
                }
                Object paraObject = parasMap.get(key);
                if (paraObject instanceof BaseDO) {
                    if (SqlCommandType.UPDATE == sqlCommandType) {
                        Date updateTime = new Date();
                        BeanUtils.setProperty(paraObject, "updatedBy", userName);
                        BeanUtils.setProperty(paraObject, "updateTimestamp", updateTime);
                        continue; }}}// Compatible with batch insertion
            if (object instanceof DefaultSqlSession.StrictMap) {
                logger.debug("mybatis arg: {}", object);
                @SuppressWarnings("unchecked")
                DefaultSqlSession.StrictMap<ArrayList<Object>> map = (DefaultSqlSession.StrictMap<ArrayList<Object>>) object;
                String key = "collection";
                if(! map.containsKey(key)) {continue;
                }
                ArrayList<Object> objs = map.get(key);
                for (Object obj : objs) {
                    if (obj instanceof BaseDO) {
                        if (SqlCommandType.INSERT == sqlCommandType) {
                            Date insertTime = new Date();
                            BeanUtils.setProperty(obj, "insertedBy", userName);
                            BeanUtils.setProperty(obj, "insertTimestamp", insertTime);
                            BeanUtils.setProperty(obj, "updatedBy", userName);
                            BeanUtils.setProperty(obj, "updateTimestamp", insertTime);
                        }
                        if (SqlCommandType.UPDATE == sqlCommandType) {
                            Date updateTime = new Date();
                            BeanUtils.setProperty(obj, "updatedBy", userName);
                            BeanUtils.setProperty(obj, "updateTimestamp", updateTime);
                        }
                    }
                }
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}}Copy the code

Through the code above you can see, we are a custom interceptors IbatisAuditDataInterceptor Interceptor interface is realized.

In the @intercepts annotation on our interceptor, the type parameter specifies that the class being intercepted is an implementation of the Executor interface, and the method parameter specifies that the update method in Executor is intercepted because database operations are added, deleted, or modified through the update method.

Configure the interceptor plug-in

After you have defined the interceptor, you need to specify it in plugins for the SqlSessionFactoryBean to take effect. Therefore, perform the following operations.

<bean id="transSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="transDataSource" />
    <property name="mapperLocations">
        <array>
            <value>classpath:META-INF/mapper/*.xml</value>
        </array>
    </property>
    <property name="plugins">
        <array>
            <! -- Handle audit fields -->
            <ref bean="ibatisAuditDataInterceptor" />
        </array>
    </property>
Copy the code

At this point, our custom interceptor takes effect. By testing it, you can see that instead of manually setting the value of the audit field in the business code, it is automatically assigned to the audit field by the interceptor plug-in after the transaction commits.

summary

In this installment, Hei shows you how to gracefully handle updates to audit fields that are frequently used in our daily development.

Through the custom MyBatis interceptor, it can automatically assign values to some business models with audit fields in the form of plug-ins, avoiding repetitive boring code writing.

After all, life is too short to write code and fish.

If this article has been helpful to you, give little Black a thumbs up.

I am xiao Hei, a programmer in the Internet “casual”

Water does not compete, you are in the flow