This is the 21st day of my participation in the August Text Challenge.More challenges in August

Mybatis- Plus provides some general mapper methods, such as insert, update, selectById, etc. By inheriting the BaseMapper class from Mybatis- Plus, we can directly call some basic SQL methods without writing our own SQL.

public interface BaseMapper<T> extends Mapper<T> {}
Copy the code

However, in the process of using it, we found that the methods provided are a little sparse. When we want to add our own general SQL methods, we can use the SQL injector described in the official documentation to do so. Than we ourselves define a saveBatch method, used to batch insert data.

BaseMapper custom extension

Mybatis – Plus provides the ISqlInjector interface as well as the AbstractSqlInjector class. We can add custom Mapper methods by implementing the interface, or by inheriting our own SQL logic from abstract classes, and then inheriting BaseMapper to add the methods we need.

In addition to these two interfaces, mybatis- Plus actually provides us with a default implementation: DefaultSqlInjector, which already contains some methods in BaseMapper that mybatis- Plus has already packaged. If we want to extend, we can directly extend this class to add our methods.

Here we want to add a saveBatch method to BaseMapper, which can be used to insert data in batches:

  1. implementationDefaultSqlInjectorClass, we can see thatYou need to implementgetMethodListmethods, the method argument is the class of the Mapper interface and the return value is a List<AbstractMethod>. So our custom method is required to implementAbstractMethod. Some are already implemented in Mybatis – PlusAbstractMethodMethod, we mock up a SaveBatch class.
public class CustomSqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class
        mapperClass) {
      	// The list of the parent class already contains the basic methods of BaseMapper.
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // Add the custom methods we need to add.
        methodList.add(new SaveBatch());
        returnmethodList; }}Copy the code
  1. Implement the logic of the SaveBatch class (this is the official samples). As you can see, the main logic here is to generate the MappedStatement object.
public class SaveBatch extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class
        mapperClass, Class
        modelClass, TableInfo tableInfo) {
        final String sql = "<script>insert into %s %s values %s</script>";
        final String fieldSql = prepareFieldSql(tableInfo);
        final String valueSql = prepareValuesSqlForMysqlBatch(tableInfo);
        final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, "saveBatch", sqlSource, new NoKeyGenerator(), null.null);
    }


    private String prepareFieldSql(TableInfo tableInfo) {
        StringBuilder fieldSql = new StringBuilder();
        fieldSql.append(tableInfo.getKeyColumn()).append(",");
        tableInfo.getFieldList().forEach(x -> {
            fieldSql.append(x.getColumn()).append(",");
        });
        fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
        fieldSql.insert(0."(");
        fieldSql.append(")");
        return fieldSql.toString();
    }


    private String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) {
        final StringBuilder valueSql = new StringBuilder();
        valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");
        valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
        tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
        valueSql.delete(valueSql.length() - 1, valueSql.length());
        valueSql.append("</foreach>");
        returnvalueSql.toString(); }}Copy the code
  1. Finally we need to inject our Injector into the Spring container, replacing the default Injector.
@Bean
public CustomSqlInjector myLogicSqlInjector(a) {
    return new CustomSqlInjector();
}
Copy the code
  1. Validation:
public interface TB3Mapper extends MyBaseMapper<Tb3> {}@Test
public void test(a) {
    List<Tb3> tb3s = Arrays.asList(Tb3.getInstance(), Tb3.getInstance());
    tb3Mapper.saveBatch(tb3s);
}
// output log
==>  Preparing: insert into tb3 (id,f1,f2,f3) values (? ,? ,? ,?).(? ,? ,? ,?)
==> Parameters: 38(Integer), 62(Integer), -1546785812(Integer), -16950756(Integer), 24(Integer), 17(Integer), -1871764773(Integer), 169785869(Integer)
<==    Updates: 2
Copy the code

The principle of analytic

First of all, I simply said mybatis- Plus, I just looked at the source code, mybatis- Plus working principle is: a comprehensive again agent of mybatis some things. Such as automatic configuration to adopt the MybatisPlusAutoConfiguration SqlSessionFactoryBean adopted MybatisSqlSessionFactoryBean, etc., the core component of the mybatis, All were replaced by Mybatis plus, which made it its own, and then it customized its own logic in its own.

I just analyzed the implementation principle of BaseMapper. I didn’t see this in the document at the beginning, so I wrote a version of self-defined logic by hand and tracked this code. Before this or a simple explanation of mybatis some core principles, if you have not read myBatis source code, know these should also be able to understand.

The overall logic of Mybatis can be divided into two parts:

  1. Configuration file parsing: This process includes parsing our config configuration, as well as the mapper.xml file. The final Configuration is parsed into a Configuration object, and each subsequent SqlSession contains a reference to the Configuration object instance. There are two important things in this Configuration:

  • MappedStatements: stores SQL information corresponding to mapper
  • Deposit mybatisMapperRegistry. KnownMappers: mapper interfaces corresponds to the proxy class

These two things run through the mybatis interface to SQL execution logic.

  1. Interface call: Our interface is actually calling the wrapper class of the proxy, . This class is above mybatisMapperRegistry knownMappers MybatisMapperProxyFactory show inside (mybatis MapperProxyFactory) agent Mybati getObject returns SMapperProxy object. The main logic in the proxy class is to find the SQL in the mappedStatements of Configuration when specifying a method with the fully qualified class name of the class.

So knowing the general logic of Mybatis, we can guess: when the Configuration is loaded, there must be somewhere to load the information of SQL corresponding to our BaseMapper default method into the mappedStatements map. We need to keep track of where we build the MappedStatement object for these default base methods and insert it into the Configuration.

The Debug trace can find:

As a first step, it must be automatically configured to load SqlSessionFactory, this method is mainly to build MybatisSqlSessionFactoryBean object, then call getObject method, We follow up MybatisSqlSessionFactoryBean. GetObject ()

@Override
public SqlSessionFactory getObject(a) throws Exception {
    if (this.sqlSessionFactory == null) {
        afterPropertiesSet();
    }
    return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet(a) throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    state((configuration == null && configLocation == null) | |! (configuration ! =null&& configLocation ! =null),
        "Property 'configuration' and 'configLocation' can not specified with together");
  	// This is where we start building the SqlSessionFactory
    this.sqlSessionFactory = buildSqlSessionFactory();
}
Copy the code

As you can see, buildSqlSessionFactory() is finally executed. The main logic of this approach is to parse the XML Configuration to create a Configuration object. We can find the logic for parsing our mapper.xml file at the bottom:

if (this.mapperLocations ! =null) {
    if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
    } else {
        for (Resource mapperLocation : this.mapperLocations) {
            if (mapperLocation == null) {
                continue;
            }
            try {
              	// Parse each mapper. XML file
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                    targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                xmlMapperBuilder.parse();
            } catch (Exception e) {
                throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
            } finally {
                ErrorContext.instance().reset();
            }
            LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); }}}else {
    LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
Copy the code

Xmlmapperbuilder.parse ();

public void parse(a) {
  if(! configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    // debug finds that the number of mapper methods increases after mappedStatements in Configuration execute this method.
    bindMapperForNamespace();
  }
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
Copy the code

In bindMapperForNamespace, it’s configuration.addMapper(boundType); And then there are more methods. . This method is invoked eventually MybatisMapperRegistry addMapper (), this method will eventually turn to call MybatisMapperAnnotationBuilder. The parse () method, Add mapper’s methods to mappedStatements.

@Override
public void parse(a) {...try {
            // https://github.com/baomidou/mybatis-plus/issues/3038
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
              	// After this step, mappestatment is addedparserInjector(); }}catch (IncompleteElementException e) {
            configuration.addIncompleteMethod(new InjectorResolver(this));
        }
    }
    parsePendingMethods();
}
Copy the code

The parserInjector method looks like this:

void parserInjector(a) {
    GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
//GlobalConfigUtils.getSqlInjector
public static ISqlInjector getSqlInjector(Configuration configuration) {
    return getGlobalConfig(configuration).getSqlInjector();
}
//getSqlInjector()
private ISqlInjector sqlInjector = new DefaultSqlInjector();
//MybatisPlusAutoConfiguration.sqlSessionFactory#sqlInjector
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
Copy the code

As we can see, there are a series of methods to get the ISqlInjector implementation class. The default is DefaultSqlInjector, but if the implementation class is manually injected into Spring, it will be changed to our custom SqlInjector during automatic configuration.

This will go to our custom logic, but our CustomSqlInjector extends DefaultSqlInjector, so the logic is still in the DefaultSqlInjector.

//DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class
        mapperClass) {
    return Stream.of(
        new Insert(),
        new Delete(),
      //....
    ).collect(toList());
}
//AbstractSqlInjector
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class
        mapperClass) { Class<? > modelClass = extractModelClass(mapperClass);if(modelClass ! =null) {
        String className = mapperClass.toString();
        Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
        if(! mapperRegistryCache.contains(className)) {// Here we take AbstractMethod List returned by our CustomSqlInjector and loop over inject
            List<AbstractMethod> methodList = this.getMethodList(mapperClass);
            if (CollectionUtils.isNotEmpty(methodList)) {
                TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                // Loop to inject custom methods
                methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
            } else {
                logger.debug(mapperClass.toString() + ", No effective injection method was found."); } mapperRegistryCache.add(className); }}}//AbstractMethod
public void inject(MapperBuilderAssistant builderAssistant, Class
        mapperClass, Class
        modelClass, TableInfo tableInfo) {
    this.configuration = builderAssistant.getConfiguration();
    this.builderAssistant = builderAssistant;
    this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
    /* Inject custom methods */
    injectMappedStatement(mapperClass, modelClass, tableInfo);
}
Copy the code

This makes it clear that a set of template method pattern classes reserve hooks for subclasses to implement.

DefaultSqlInjector This class simply provides what methods need to be injected into mappedStatements. This list will be called by the abstract class AbstractSqlInjector hook.

AbstractSqlInjector is basically a List of AbstractMethods returned by the getMethodList loop, and then calls the Inject method.

AbstractMethod inject is our self-defined logic.

SaveBatch After building the elements required by an MappedStatement object, addInsertMappedStatement is inserted into the mappedStatements in the Configuration.

Analysis done.

One day

Mybatis – Plus has some public methods in addition to BaseMapper, which are placed in a ServiceImpl class. Many people inherit this class from the service layer to get these functions, which I have always disliked:

  • Inheriting this from the Service layer feels like migrating the functionality of the Dao to the Service layer, and the hierarchy is a bit uncomfortable (though it doesn’t really matter).
  • Many of the methods in ServiceImpl are forced to have transaction annotations, which we can’t change! This is bad, because with multiple data sources these transaction annotations will cause the data source switch to fail.

My thinking is can these methods fall into the BaseMapper layer again? After this analysis, it is found that it is not appropriate: the basic methods in BaseMapper generally correspond to this SQL, which can be completely built.

However, many of the methods in ServiceImpl actually package multiple SQL queries and commit them to flush. Even a saveOrUpdate method executes a query, processes it, and then updates or inserts it. None of this can be done by a single SQL, so Mybatis – Plus keeps this logic in ServiceImpl.

If you want to keep these methods in BaseMapper, you may need to specifically modify the MapperProxy proxy class. In comparison, ServiceImpl is a good choice.