MyBatis добавляет метаданные в теги пользовательских элементов

MyBatis

История развития

 Существующая система поддерживает набор метаданных о столбцах и ключах бизнес-таблиц и надеется автоматически инкапсулировать операторы SQL и настраивать стратегии первичных ключей путем чтения метаданных. План внедрения состоит в том, чтобы инвазивно изменить MyBatis и добавить теги элементов.meta, которые можно использовать в файлах сопоставления XML для поддержки развития бизнеса.

Метаэлемент устроен следующим образом:

<!-- meta标签 可根据参数获取到对应的表名 动态生成语句 -->
<!ELEMENT meta EMPTY>
<!ATTLIST meta
test CDATA #IMPLIED
type (update|insert|select|columns|pk-col|load|load-columns) #IMPLIED
ignore CDATA #IMPLIED
table CDATA #IMPLIED
func CDATA #IMPLIED
alias CDATA #IMPLIED
>

Пример ожиданий выглядит следующим образом:

<insert id="insertMap" useGeneratedKeys="true" generator="meta">
    <meta table="USER" type="insert"/>
</insert>

<update id="updateMap">
    <meta table="USER" type="update"/>
</update>

<select id="selectOneByPk" resultType="java.util.HashMap">
    select 
    <meta table="USER" type="columns"/> 
    from USER 
    where <meta table="USER" type="pk-col"/> = #{__PK_VALUE}
</select>

подготовка к разработке

  Создайте новый проект и введите две основные зависимости, mybatis и mybatis-spring.

<!-- mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
</dependency>
<!-- mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
</dependency>

Добавить пользовательские элементы

Создайте MetaHandler и MetaSqlNode

public class MetaHandler implements NodeHandler {

    private final CustomConfiguration configuration;

    protected MetaHandler(CustomConfiguration configuration) {
        this.configuration = configuration;
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        final String test = nodeToHandle.getStringAttribute("test");
        final String type = nodeToHandle.getStringAttribute("type");
        final String ignore = nodeToHandle.getStringAttribute("ignore");
        final String table = nodeToHandle.getStringAttribute("table");
        final String func = nodeToHandle.getStringAttribute("func");
        String alias = nodeToHandle.getStringAttribute("alias");
        if (!StringUtils.isEmpty(alias)) {
            alias = alias.trim();
            //是否无效 防止注入
            boolean invalid = alias.contains(" ") || alias.contains(".");
            if (invalid) {
                throw new RuntimeException("alias is invalid : " + alias);
            }
        }
        MetaSqlNode metaSqlNode = new MetaSqlNode(configuration, test, type, ignore, table, func, alias);
        targetContents.add(metaSqlNode);
    }
}
public class MetaSqlNode implements SqlNode {

    /**
     * mybatis核心数据
     */
    private final CustomConfiguration configuration;
    /**
     * 判断语句校验器
     */
    private final ExpressionEvaluator evaluator;
    /**
     * 判断语句,同if标签
     */
    private final String test;
    /**
     * 生成语句类型 update|insert|select|columns|pk-col|load|load-columns
     */
    private final TypeEnum type;
    /**
     * 忽略的列
     */
    private final String ignore;
    /**
     * 表名,未指定则从调用参数中获取
     */
    private final String table;
    /**
     * 功能,未指定则从调用参数中获取
     */
    private final String func;
    /**
     * 动态列别名
     */
    private final String alias;

    public MetaSqlNode(CustomConfiguration configuration, String test, String type, String ignore, String table, String func, String alias) {
        this.evaluator = new ExpressionEvaluator();
        this.configuration = configuration;
        this.test = test;
        this.type = TypeEnum.parse(type);
        this.ignore = ignore;
        this.table = table;
        this.func = func;
        this.alias = alias;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // TODO 解析type与table,向context中添加语句
        context.appendSql(" insert ······  ");
    }
}

Создать пользовательскийXMLScriptBuilder

 Контент скопирован сorg.apache.ibatis.scripting.xmltags.XMLScriptBuilder, добавьте MetaHandler в метод initNodeHandlerMap.

private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
    //增加元数据标签解析器
    if (configuration instanceof CustomConfiguration) {
        nodeHandlerMap.put("meta", new MetaHandler((CustomConfiguration) configuration));
    }
}

Создать CustomXMLLanguageDriver

 Контент скопирован сorg.apache.ibatis.scripting.xmltags.XMLLanguageDriver, используйте CustomXMLScriptBuilder в методе createSqlSource для анализа Xml для создания SqlSource.

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    CustomXMLScriptBuilder builder = new CustomXMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

Создать пользовательскую конфигурацию

Наследованиеorg.apache.ibatis.session.Configuration, содержимое копируется из Configuration. Измените XMLLanguageDriver в конструкторе на CustomConfiguration.

public CustomConfiguration() {
    
    ······
    
    //默认使用自定义 LanguageDriver
    typeAliasRegistry.registerAlias("XML", CustomXMLLanguageDriver.class);
    
    ······
    
    //默认使用自定义 LanguageDriver
    languageRegistry.setDefaultDriverClass(CustomXMLLanguageDriver.class);
    
    ······
    
}

Создать CustomXMLConfigBuilder

 Контент скопирован сorg.apache.ibatis.builder.xml.XMLConfigBuilder, который поддерживает создание пользовательских конфигураций с помощью XML-конфигурации.

public class CustomXMLConfigBuilder extends BaseBuilder {
    
    ······
    
    private CustomXMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        // 使用 CustomConfiguration
        super(new CustomConfiguration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
    }
    
    ······
    
}

Создать фабрику SqlSessionFactory

 Скопировано изorg.mybatis.spring.SqlSessionFactoryBean, замените Configuration в методе buildSqlSessionFactory на CustomConfiguration.

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    CustomXMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
        targetConfiguration = this.configuration;
        if (targetConfiguration.getVariables() == null) {
            targetConfiguration.setVariables(this.configurationProperties);
        } else if (this.configurationProperties != null) {
            targetConfiguration.getVariables().putAll(this.configurationProperties);
        }
    } else if (this.configLocation != null) {
        // 使用 CustomXMLConfigBuilder 创建 CustomConfiguration
        xmlConfigBuilder = new CustomXMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
        targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
        LOGGER.debug(
                () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
        // 使用 CustomConfiguration
        targetConfiguration = new CustomConfiguration();
        Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
    }
    
    ······
    
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

Изменить ограничения DTD

 Файл ограничений MyBatis не поддерживает пользовательские метаэлементы и должен обрабатываться с использованием CDATA. Пример выглядит следующим образом:

<insert id="insertMap" useGeneratedKeys="true" generator="meta">
  <![CDATA[[
    <meta table="USER" type="insert"/>
  ]]>
</insert>

 Если вы не хотите записывать CDATA, вам нужно изменить ограничения DTD.Можно сделать следующие два способа, далее в основном речь идет о втором способе переписывания кода..

  • Добавьте файл ограничений DTD в указанное место в классах.org/apache/ibatis/builder/xml/mybatis-3-config.dtdДобейтесь эффекта покрытия MyBatis DTD.
  • Перепишите код для использования указанного DTD.

Создать CustomXMLMapperEntityResolver

 Скопировано изorg.apache.ibatis.builder.xml.XMLMapperEntityResolver,БудуMYBATIS_MAPPER_DTDИзмените, указав на локальный файл mybatis-3-mapper.dtd, и добавьте ограничения метаэлемента в файл DTD.

public class CustomXMLMapperEntityResolver implements EntityResolver {
    
    ······
    
    private static final String MYBATIS_MAPPER_DTD = "com/my/ibatis/builder/xml/mybatis-3-mapper.dtd";
    
    ······
    
}
<!-- meta标签 可根据参数获取到对应的表名 动态生成语句 -->
<!ELEMENT meta EMPTY>
<!ATTLIST meta
test CDATA #IMPLIED
type (update|insert|select|columns|pk-col|load|load-columns) #IMPLIED
ignore CDATA #IMPLIED
table CDATA #IMPLIED
func CDATA #IMPLIED
alias CDATA #IMPLIED
>

CustomXMLLanguageDriver

  Обработка аннотаций динамических операторов Mapper использует CustomXMLMapperEntityResolver.

/**
 * Mapper动态语句注解调用
 * <p>
 * "<script>select * from user <if test=\"id !=null \">where id = #{id} </if></script>"
 *
 * @param configuration mybatis配置
 * @param script        动态语句字符串
 * @param parameterType 参数类型
 * @return org.apache.ibatis.mapping.SqlSource
 */
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
        //将动态语句字符串转换为XNode对象
        XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new CustomXMLMapperEntityResolver());
        return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
        // issue #127
        script = PropertyParser.parse(script, configuration.getVariables());
        TextSqlNode textSqlNode = new TextSqlNode(script);
        if (textSqlNode.isDynamic()) {
            return new CustomDynamicSqlSource(configuration, textSqlNode);
        } else {
            return new RawSqlSource(configuration, script, parameterType);
        }
    }
}

Создать CustomXMLMapperBuilder

  скопировано изorg.apache.ibatis.builder.xml.XMLMapperBuilder, измените конструктор, чтобы использовать CustomXMLMapperEntityResolver для синтаксического анализа XML.

public CustomXMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new CustomXMLMapperEntityResolver()),
            configuration, resource, sqlFragments);
}

SqlSessionFactory

  Измените метод buildSqlSessionFactory, чтобы использовать CustomXMLMapperBuilder для анализа XML.

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    ······
        try {
            //使用自定义 XMLMapperBuilder
            CustomXMLMapperBuilder xmlMapperBuilder = new CustomXMLMapperBuilder(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();
        }
        
    ······
    
}

Создать CustomMapperAnnotationBuilder

 Скопировано изorg.apache.ibatis.builder.annotation.MapperAnnotationBuilder, измените метод loadXmlResource, чтобы использовать CustomXMLMapperBuilder.

private void loadXmlResource() {
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        
    ······
    
        if (inputStream != null) {
            //使用自定义解析器支持自定义标签
            CustomXMLMapperBuilder xmlParser = new CustomXMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            xmlParser.parse();
        }
    }
}

Создать CustomMapperRegistry

  скопировано изorg.apache.ibatis.binding.MapperRegistry, измените метод addMapper, чтобы использовать CustomMapperAnnotationBuilder.

@Override
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        
    ······
    
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            CustomMapperAnnotationBuilder parser = new CustomMapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

CustomConfiguration

  Измените свойство mapperRegistry, чтобы использовать CustomMapperRegistry.


public class CustomConfiguration extends Configuration {
        
    ······

    protected final MapperRegistry mapperRegistry = new CustomMapperRegistry(this);
        
    ······
    
}

Использование весной

<!-- Mybatis SessionFactory-->
<bean id="sqlSessionFactory" class="com.my.ibatis.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configurationProperties" >
        <bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">
            <property name="locations" value="classpath*:mybatis.properties"/>
        </bean>
    </property>
</bean>

@Configuration
public class MybatisConfig {
    @Bean
    public PropertiesFactoryBean createPropertiesFactoryBean() throws IOException {
        PropertiesFactoryBean bean = new PropertiesFactoryBean();
        bean.setLocation(new ClassPathResource("mybatis.properties"));
        return bean;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean createSqlSessionFactory(DataSource dataSource, PropertiesFactoryBean bean) throws IOException {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setConfigurationProperties(bean.getObject());
        return factoryBean;
    }
}

Следующая статья: Генерация тегов метаданных MyBatis SQL