Happy Lantern Festival! Remember to eat yuanxiao

In daily development, friends more or less have used MyBatis plug-in, Songge guess we use the most is MyBatis paging plug-in! Have you ever thought of developing a MyBatis plugin yourself?

In fact, it is not difficult to masturbate a MyBatis plug-in. Today, Songge will take you to masturbate a MyBatis plug-in!

1.MyBatis plug-in interface

If you haven’t developed MyBatis, you can probably guess that the MyBatis plugin works by interceptor. The MyBatis framework was designed to provide interfaces for plugin development.

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP}}Copy the code

There are only three methods in this interface. The first method must be implemented, and the second two methods are optional. The functions of the three methods are as follows:

  1. Intercept: This is the specific interception method. When we customize MyBatis plug-in, we generally need to rewrite this method. The work completed by our plug-in is also completed in this method.

  2. Plugin: The target parameter of this method is the object to be intercepted by the interceptor. Generally we do not need to override this method. The plugin.wrap method automatically determines whether the interceptor’s signature matches the interceptor’s interface, and if so, intercepts the target object via a dynamic proxy.

  3. SetProperties: This method is used to pass plug-in parameters that can be used to change the behavior of the plug-in. After the plug-in is defined, it is necessary to configure the plug-in. During the configuration, you can set related properties for the plug-in, and the set properties can be obtained through this method. Plugin properties are set like this:

<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
        <property name="xxx" value="xxx"/>
    </plugin>
</plugins>
Copy the code

2.MyBatis interceptor signature

Once the interceptor is defined, who does it intercept?

This requires interceptor signatures!

The interceptor Signature is an annotation named @intercepts in which multiple signatures can be configured using @signature. The @signature annotation contains three attributes:

  • Type: specifies the interface that the interceptor intercepts. The options are Executor, ParameterHandler, ResultSetHandler, and StatementHandler.
  • Method: The name of the method on the interface intercepted by the interceptor.
  • Args: The parameter type of the method intercepted by the interceptor. The method name and parameter type lock a unique method.

A simple signature might look like this:

@Intercepts(@Signature( type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class} ))
public class CamelInterceptor implements Interceptor {
    / /...
}
Copy the code

3. Intercepted objects

According to the previous introduction, there are four intercepted objects:

Executor

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements(a) throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache(a);

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class
        targetType);

  Transaction getTransaction(a);

  void close(boolean forceRollback);

  boolean isClosed(a);

  void setExecutorWrapper(Executor executor);

}
Copy the code

The meanings of each method are as follows:

  • Update: This method is called during all INSERT, update, and DELETE operations and can be used to intercept these operations.
  • Query: This method is called when the SELECT query method is executed. The method parameters carry a lot of useful information that can be retrieved if needed.
  • QueryCursor: This method is called when the return type of the SELECT is Cursor.
  • FlushStatements: This method is triggered when the SqlSession method calls the flushStatements method or executes an interface method with the @flush annotation.
  • Commit: This method is triggered when the SqlSession method calls commit.
  • Rollback: This method is triggered when the SqlSession method calls the ROLLBACK method.
  • GetTransaction: This method is triggered when the SqlSession method obtains a database connection.
  • Close: This method is triggered after lazy loading acquires a new Executor.
  • IsClosed: This method is triggered before lazy loading to execute the query.

ParameterHandler

public interface ParameterHandler {

  Object getParameterObject(a);

  void setParameters(PreparedStatement ps) throws SQLException;

}
Copy the code

The meanings of each method are as follows:

  • GetParameterObject: This method is triggered when a stored procedure processes an outbound parameter.
  • SetParameters: This method is triggered when SQL parameters are set.

ResultSetHandler

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}
Copy the code

The meanings of each method are as follows:

  • HandleResultSets: This method is triggered in all query methods (except those that return values of type Cursor). In general, if we want to perform secondary processing on the query result, we can do so by intercepting this method.
  • HandleCursorResultSets: This method is triggered when the query method returns a value of type Cursor.
  • HandleOutputParameters: This method is called when a parameter is processed using a stored procedure.

StatementHandler

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql(a);

  ParameterHandler getParameterHandler(a);

}
Copy the code

The meanings of each method are as follows:

  • Prepare: The method is triggered before the database is executed.
  • Parameterize: This method is executed after the prepare method and is used to process parameter information.
  • Batch: If the MyBatis full play configuration is configuredBATCH defaultExecutorType = ""This method is called when a data operation is performed.
  • Update: This method is triggered during an update operation.
  • Query: This method is triggered when the SELECT method executes.
  • QueryCursor: This method is fired when the SELECT method executes and returns a value of Cursor.

When developing a specific plug-in, we should decide which method to intercept based on our own requirements.

4. Develop paging plug-ins

4.1 Memory Paging

MyBatis provides a very difficult memory paging function, which is to query all data at once and then perform paging in memory. This method is inefficient and basically useless, but if we want to customize the paging plug-in, we need to have a simple understanding of this method.

Paging is used by adding the RowBounds parameter to Mapper as follows:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}
Copy the code

Then define the associated SQL in the XML file:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>
Copy the code

MyBatis will query all the data and perform paging in memory.

Methods in Mapper are called as follows:

@Test
public void test3(a) {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    RowBounds rowBounds = new RowBounds(1.2);
    List<User> list = userMapper.getAllUsersByPage(rowBounds);
    for (User user : list) {
        System.out.println("user = "+ user); }}Copy the code

The RowBounds are built with two parameters, offset and limit, which correspond to the two parameters in the paging SQL. You can also build a RowBounds instance using rowbound. DEFAULT, which creates a RowBounds instance with offset to 0 and limit to integer. MAX_VALUE, which means no paging.

This is a very impractical memory paging feature provided in MyBatis.

Now that you know about MyBatis’ built-in memory paging, let’s look at how to customize the paging plug-in.

4.2 Custom paging plug-ins

First of all, we want to declare that here songge lead you to customize MyBatis page plug-in, mainly want to let friends understand some rules of custom MyBatis plug-in through this thing, understand the whole process of custom plug-in, page plug-in is not our goal. The custom paging plug-in is just to make the learning process more interesting.

Let’s begin the journey of custom paging plug-ins.

First we need to customize the RowBounds, because MyBatis’ native RowBounds are memory paged, and there is no way to get the total number of bounds (we need to get the total number of bounds for general paging queries), so we customize the PageRowBounds, Enhancements to the native RowBounds feature are as follows:

public class PageRowBounds extends RowBounds {
    private Long total;

    public PageRowBounds(int offset, int limit) {
        super(offset, limit);
    }

    public PageRowBounds(a) {}public Long getTotal(a) {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total; }}Copy the code

As you can see, we added the total field to our custom PageRowBounds to hold the total number of records for the query.

Next we define the PageInterceptor as follows:

@Intercepts(@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ))
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        if(rowBounds ! = RowBounds.DEFAULT) { Executor executor = (Executor) invocation.getTarget(); BoundSql boundSql = ms.getBoundSql(parameterObject); Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
            Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
            if (rowBounds instanceof PageRowBounds) {
                MappedStatement countMs = newMappedStatement(ms, Long.class);
                CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
                String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
                BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
                Set<String> keySet = additionalParameters.keySet();
                for (String key : keySet) {
                    countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }
                List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], countKey, countBoundSql);
                Long count = (Long) countQueryResult.get(0);
                ((PageRowBounds) rowBounds).setTotal(count);
            }
            CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
            pageKey.update("RowBounds");
            String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
            Set<String> keySet = additionalParameters.keySet();
            for (String key : keySet) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
            return list;
        }
        // Return the result directly without pagination
        return invocation.proceed();
    }

    private MappedStatement newMappedStatement(MappedStatement ms, Class<Long> longClass) {
        MappedStatement.Builder builder = new MappedStatement.Builder(
                ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType()
        );
        ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build();
        builder.resource(ms.getResource())
                .fetchSize(ms.getFetchSize())
                .statementType(ms.getStatementType())
                .timeout(ms.getTimeout())
                .parameterMap(ms.getParameterMap())
                .resultSetType(ms.getResultSetType())
                .cache(ms.getCache())
                .flushCacheRequired(ms.isFlushCacheRequired())
                .useCache(ms.isUseCache())
                .resultMaps(Arrays.asList(resultMap));
        if(ms.getKeyProperties() ! =null && ms.getKeyProperties().length > 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        returnbuilder.build(); }}Copy the code

This is the core code that we define today, and songo will give you an analysis of the knowledge points involved.

  1. First configure the interceptor Signature using the @intercepts annotation. As you can see from the @signature definition, the interceptor Intercepts the Executor# Query method, which has an overloaded method that specifies method parameters via args. This locks the overloaded method (in fact, another overloaded method of this method can not be intercepted, that is MyBatis internal call, not discussed here).
  2. After intercepting the query, we do most of our work in the PageInterceptor#intercept method, whose parameters contain a lot of information about the intercepting object.
  3. throughinvocation.getArgs()Gets the argument to the interceptor method, and returns an array that is normally 4 in length. The first item in the array is an MappedStatement. The various operation nodes and SQL defined in mapper. XML are encapsulated into MappedStatement objects. The second element of the array is the argument to the intercepted method, which you defined in the Mapper interface; The third item in the array is a RowBounds object. We don’t necessarily use a RowBounds object when defining methods in the Mapper interface. If we don’t define a RowBounds object, we are given a RowBounds object by DEFAULT. The fourth entry in the array is a ResultHandler that handles the returned value.
  4. Next check whether the rowBounds object extracted in the previous step is not rowBounds.DEFAULT. If rowBounds. If it is not rowbounds. DEFAULT, the user wants paging, and if the user does not want paging, the last one is executed directlyreturn invocation.proceed();, let the method continue down the line.
  5. If paging is required, the Executor, BoundSql, and the extra parameters saved in BoundSql (which might exist if we were using dynamic SQL) are retrieved from the Invocation object first. BoundSql encapsulates the Sql we are executing and the associated parameters.
  6. Then check whether rowBounds is an instance of PageRowBounds. If so, you want to query the total number of bounds, if not, you want to query the total number of bounds.
  7. To query the total number of records, a newMappedStatement object is constructed by calling the newMappedStatement method. The newMappedStatement object returns a value of type Long. We then create the CacheKey for the query, concatenate the countSql for the query, build countBoundSql from countSql, and add additional parameters to countBoundSql. The executor.query method completes the query and assigns the result to the Total property in PageRowBounds.
  8. After the introduction of step 7, paging queries are very simple and will not be covered here, the only thing to note is that MyBatis’ native RowBounds memory pages become physical pages when we enable the paging plug-in. This is why we changed the query SQL.
  9. Finally, the query result is returned.

In the previous code, we reorganized the SQL in mapper.xml using boundSQL.getsql () to retrieve the SQL from mapper.xml. Mapper.xml: mapper.xml: mapper.xml: mapper.xml: mapper.xml: mapper.xml: mapper.xml: mapper.xml This will cause the SQL reassembled by the paging plug-in to run incorrectly, which is important to note. The same is true for other MyBatis paging plug-ins songo saw on GitHub. Mapper.xml does not have SQL endings; .

With that in mind, our paging plug-in is defined successfully.

5. Test

Let’s do a simple test of our paging plug-in.

First we need to configure the paging plug-in in the global configuration as follows:

<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.PageInterceptor"></plugin>
</plugins>
Copy the code

Next we define the query interface in Mapper:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}
Copy the code

Next, define usermapper.xml as follows:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>
Copy the code

Finally, we tested:

@Test
public void test3(a) {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    List<User> list = userMapper.getAllUsersByPage(new RowBounds(1.2));
    for (User user : list) {
        System.out.println("user = "+ user); }}Copy the code

Here, we use the RowBounds object for the query, which only pages rather than counting the total number of records. Note that the paging is not memory paging, but physical paging, as we can see from the printed SQL, as follows:

As you can see, the query is already paginated.

Of course, we can also test using PageRowBounds as follows:

@Test
public void test4(a) {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    PageRowBounds pageRowBounds = new PageRowBounds(1.2);
    List<User> list = userMapper.getAllUsersByPage(pageRowBounds);
    for (User user : list) {
        System.out.println("user = " + user);
    }
    System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
}
Copy the code

We get the total number of records using the pagerowbounds.getTotal () method.

6. Summary

Well, today I mainly share with friends how we develop a MyBatis plug-in by ourselves. The functions of the plug-in are actually secondary, and the most important thing is that I hope friends can understand the working process of MyBatis.