Принцип реализации BaseMapper MyBatis-Plus

задняя часть MyBatis

Mybatis-plus предоставляет нам некоторые общие методы сопоставления, такие как вставка, обновление, selectById и т. д. Заставив наше средство сопоставления наследовать класс BaseMapper, мы можем напрямую вызывать некоторые базовые методы sql, не создавая sql самостоятельно.

public interface BaseMapper<T> extends Mapper<T> {}

Однако в процессе его использования я все же обнаружил, что представленные здесь методы немного маловаты.Когда мы хотим добавить свой собственный общий метод SQL, мы можем описать его через официальный документ.Инжектор SQLреализовать. Затем мы сами определяем метод saveBatch для вставки данных в пакетах.

Пользовательское расширение BaseMapper

mybatis-plus обеспечиваетISqlInjectorинтерфейс иAbstractSqlInjectorабстрактный класс. Мы внедряем нашу собственную определенную логику SQL, реализуя этот интерфейс или наследуя абстрактные классы, а затем наследуя BaseMapper, чтобы добавить методы, необходимые для добавления пользовательских методов сопоставления.

В дополнение к этим двум интерфейсам mybatis-plus фактически предоставляет нам реализацию по умолчанию:DefaultSqlInjector, который уже содержит некоторые методы в BaseMapper, инкапсулированные mybatis-plus.Если мы хотим расширить, мы можем напрямую наследовать этот класс, чтобы расширять и добавлять наши методы.

Здесь мы хотим добавить метод saveBatch в BaseMapper для вставки данных в пакетах:

  1. выполнитьDefaultSqlInjectorкласс, мы видимнеобходимо реализоватьgetMethodListметод, параметр метода — это класс класса интерфейса преобразователя, а возвращаемое значение — List. Итак, наш пользовательский метод должен быть реализованAbstractMethod. Вы можете обратиться к некоторым уже реализованным в mybatis-plusAbstractMethodмы имитируем и пишем класс SaveBatch.
public class CustomSqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
      	// 父类的list已经包含了BaseMapper的基础方法。
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // 添加我们需要增加的自定义方法。
        methodList.add(new SaveBatch());
        return methodList;
    }
}
  1. Реализуйте логику класса SaveBatch (это официальные примеры). Мы видим, что логика здесь в основномСоздать объект MappedStatement.
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>");
        return valueSql.toString();
    }

}
  1. Наконец, нам нужно внедрить наш инжектор в контейнер Spring, заменив инжектор по умолчанию.
@Bean
public CustomSqlInjector myLogicSqlInjector() {
    return new CustomSqlInjector();
}
  1. проверять:
public interface TB3Mapper extends MyBaseMapper<Tb3> {
}

@Test
public void test() {
    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

Принципиальный анализ

Прежде всего, кратко расскажу о mybatis-plus, я только что пролистал исходный код, принцип работы mybatis-plus таков: он полностью переделывает некоторые вещи mybatis. Например, MybatisPlusAutoConfiguration используется для автоматической настройки, MybatisSqlSessionFactoryBean используется для SqlSessionFactoryBean и т. д. Все эти основные компоненты mybatis были заменены mybatis-plus и используются как собственные, а затем настраиваются в собственной логике.

Я только проанализировал принцип реализации BaseMapper, в начале документа этого не увидел, написал кастомную логику и отследил этот код. Перед этим я кратко объясню некоторые из основных принципов mybatis.Если вы не видели исходный код mybatis, вы должны быть в состоянии понять их.

Общую логику mybatis можно разделить на 2 блока:

  1. Анализ файла конфигурации: этот процесс включает в себя анализ конфигурации нашей конфигурации, а также файла mapper.xml. Окончательная конфигурация будет преобразована в объект Configuration, и каждый последующий сеанс SqlSession также будет содержать ссылку на экземпляр объекта Configuration. В этой конфигурации есть две самые важные вещи:

image-20210830223002642.png

  • mappedStatements: хранить информацию sql, соответствующую картографу
  • mybatisMapperRegistry.knownMappers: хранит прокси-класс, соответствующий интерфейсу картографа.

Эти две вещи работают через интерфейс mybatis с логикой выполнения sql.

  1. Вызов интерфейса: То, что вызывает наш интерфейс, на самом деле является классом-оболочкой прокси-сервера, который также является прокси-объектом MybatisMapperProxy, возвращаемым getObject MybatisMapperProxyFactory (mybatis — это MapperProxyFactory), показанным в mybatisMapperRegistry.knownMappers выше. Основная логика в этом прокси-классе состоит в том, чтобы взять полное имя класса класса и при указании метода перейти к mappedStatements конфигурации, чтобы найти соответствующий sql.

Итак, зная общую логику mybatis, мы можем догадаться:Когда конфигурация загружена, должно быть место для загрузки информации SQL, соответствующей методу по умолчанию нашего BaseMapper, в карту mappedStatements. Нам нужно отслеживать, где мы создаем объекты MappedStatement для этих базовых методов по умолчанию, и вставлять их в конфигурацию.

Трассировку отладки можно найти:

Первым шагом является автоматическая настройка загрузки SqlSessionFactory.Этот метод в основном предназначен для создания объекта MybatisSqlSessionFactoryBean, а затем вызова метода getObject, мы будем следитьMybatisSqlSessionFactoryBean.getObject()

@Override
public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
        afterPropertiesSet();
    }
    return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() 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");
  	// 这里才是开始构建SqlSessionFactory的
    this.sqlSessionFactory = buildSqlSessionFactory();
}

Как видите, в конечном итоге он выполнится в buildSqlSessionFactory(). Основная логика этого метода заключается в анализе XML-конфигурации для создания объекта Configuration. Мы можем найти логику для разбора нашего файла mapper.xml внизу:

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 {
              	// 对每一个mapper.xml文件进行解析
                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.");
}

Предварительное предположение: здесь загружается информация sql, сосредоточьтесь наxmlMapperBuilder.parse();

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    // debug发现,Configuration中mappedStatements在执行该方法之后,mapper方法数量就变多了。
    bindMapperForNamespace();
  }
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

а такжеbindMapperForNamespaceВнутри выполняетсяconfiguration.addMapper(boundType);Тогда есть еще методы. Последний вызов этого методаMybatisMapperRegistry.addMapper(), этот метод в конечном итоге превратится в вызовMybatisMapperAnnotationBuilder.parse()добавьте метод сопоставления в mappedStatements.

@Override
public void parse() {
  	......
        try {
            // https://github.com/baomidou/mybatis-plus/issues/3038
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
              	// 执行该步骤之后,新增了mappestatment
                parserInjector();
            }
        } catch (IncompleteElementException e) {
            configuration.addIncompleteMethod(new InjectorResolver(this));
        }
    }
    parsePendingMethods();
}

Метод parserInjector выглядит следующим образом:

void parserInjector() {
    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);

Мы видим, что класс реализации ISqlInjector получается с помощью ряда методов. По умолчанию используется DefaultSqlInjector, но если класс реализации вручную внедряется в Spring, он будет изменен на наш пользовательский SqlInjector во время автоматической настройки.

image-20210830230401071.png

Здесь мы обратимся к нашей пользовательской логике, но здесь CustomSqlInjector расширяет DefaultSqlInjector, поэтому логика остается в 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)) {
          	// 可以看到这里拿取我们CustomSqlInjector返回的AbstractMethod list,然后循环调用inject
            List<AbstractMethod> methodList = this.getMethodList(mapperClass);
            if (CollectionUtils.isNotEmpty(methodList)) {
                TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                // 循环注入自定义方法
                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();
    /* 注入自定义方法 */
    injectMappedStatement(mapperClass, modelClass, tableInfo);
}

Здесь очень ясно, что ряд классов в шаблоне метода шаблона имеет зарезервированные хуки для реализации подклассами.

DefaultSqlInjectorЭтот класс предназначен только для того, чтобы указать, какие методы необходимо внедрить в mappedStatements, этот список будет абстрактным классом.AbstractSqlInjectorзвонок на крючок.

AbstractSqlInjectorВ основном циклgetMethodListВозвращенный AbstractMethod состоит из списка, а затем вызывается метод inject.

AbstractMethodизinjectТо есть, наша собственная логика.

SaveBatchПосле создания элементов, требуемых объектом MappedStatement, вызовитеaddInsertMappedStatementбудет вставлен в mappedStatements конфигурации.

Анализ завершен.

интермедия

В дополнение к BaseMapper, mybatis-plus имеет несколько общедоступных методов, которые размещены вServiceImplВ классе многие люди наследуют этот класс на сервисном уровне, чтобы получить эти функции, мне всегда не нравился такой способ:

  • Наследование этой вещи на сервисном уровне похоже на то, что функция Dao была перенесена на сервисный уровень, и иерархия немного неудобна (конечно, фактическое использование не имеет никакого эффекта).
  • Многие методы в этом ServiceImpl вынуждены быть аннотациями транзакций, и мы не можем их изменить! Это еще хуже, когда есть несколько источников данных, эти аннотации транзакций приведут к сбою переключения источника данных.

Я думаю, могут ли эти методы вернуться к слою BaseMapper? После этого анализа выясняется, что он не подходит: основные методы в BaseMapper в целом соответствуют этому SQL, и этот SQL можно полностью построить.

Однако многие методы в ServiceImpl на самом деле упаковывают несколько SQL-запросов, а затем отправляют их для операций очистки.Даже запись, такая как метод saveOrUpdate, выполняет запрос, а затем обновляет или вставляет после обработки. Это не задачи, которые может выполнить один SQL, поэтому mybatis-plus может поместить эту логику только в ServiceImpl.

Если вы хотите поместить эти методы в BaseMapper, вам может потребоваться специально изменить прокси-класс MapperProxy. Для сравнения, лучше поместить его в ServiceImpl.