Development background

The existing system maintains a set of metadata related to columns and keys of service tables. It is hoped that SQL statements can be automatically encapsulated and primary key policies can be customized by reading metadata. The implementation scheme is to modify MyBatis in an intrusive manner and add element tag meta, which can be used in XML mapping files in business development.

The meta elements are designed as follows:

<! SQL > select * from table where table name = 'table name';
<! 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
>
Copy the code

An example of expectations is as follows:

<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>
Copy the code

The development of preparation

Create a new project and introduce two core dependencies mybatis and MyBatis – Spring.

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

Add custom elements

Create MetaHandler and 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();// Whether invalid prevents injection
            boolean invalid = alias.contains("") || alias.contains(".");
            if (invalid) {
                throw new RuntimeException("alias is invalid : " + alias);
            }
        }
        MetaSqlNode metaSqlNode = newMetaSqlNode(configuration, test, type, ignore, table, func, alias); targetContents.add(metaSqlNode); }}Copy the code
public class MetaSqlNode implements SqlNode {

    /** * Mybatis */
    private final CustomConfiguration configuration;
    /** * Check statement validator */
    private final ExpressionEvaluator evaluator;
    /** * the same as the if tag */
    private final String test;
    / * * * generates statement type update | insert | | the select columns | pk - col | load | load - columns * /
    private final TypeEnum type;
    /**
     * 忽略的列
     */
    private final String ignore;
    /** * the table name, if not specified, gets */ from the call argument
    private final String table;
    /** * function, gets */ from the call argument if not specified
    private final String func;
    /** * dynamic column alias */
    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 parses type and table and adds statements to context
        context.appendSql("Insert......"); }}Copy the code

Create CustomXMLScriptBuilder

Contents copied from org. Apache. Ibatis. Scripting. Xmltags. XMLScriptBuilder, in add MetaHandler initNodeHandlerMap method.

private void initNodeHandlerMap(a) {
    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());
    // Add a metadata tag parser
    if (configuration instanceof CustomConfiguration) {
        nodeHandlerMap.put("meta".newMetaHandler((CustomConfiguration) configuration)); }}Copy the code

Create CustomXMLLanguageDriver

Contents copied from org. Apache. Ibatis. Scripting. Xmltags. XMLLanguageDriver, The CustomXMLScriptBuilder is used in the createSqlSource method to parse the Xml to generate the SqlSource.

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

Create CustomConfiguration

Inheritance org. Apache. Ibatis. Session. The Configuration, content copied from the Configuration. Change XMLLanguageDriver in the constructor to CustomConfiguration.

public CustomConfiguration(a) {· · · · · ·// Custom LanguageDriver is used by default
    typeAliasRegistry.registerAlias("XML", CustomXMLLanguageDriver.class); · · · · · ·// Custom LanguageDriver is used by defaultlanguageRegistry.setDefaultDriverClass(CustomXMLLanguageDriver.class); ......}Copy the code

Create CustomXMLConfigBuilder

Contents copied from org. Apache. Ibatis. Builder. XML. XMLConfigBuilder, support to create CustomConfiguration through XML configuration.

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

Create a SqlSessionFactory

Copy from the org. Mybatis. Spring. SqlSessionFactoryBean, will the Configuration of the replacement for CustomConfiguration buildSqlSessionFactory method.

protected SqlSessionFactory buildSqlSessionFactory(a) 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) {
        // Create a CustomConfiguration using CustomXMLConfigBuilder
        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");
        / / use CustomConfiguration
        targetConfiguration = new CustomConfiguration();
        Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables); } · · · · · ·return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
Copy the code

Modifying DTD constraints

MyBatis constraint files do not support custom meta elements and need to be handled using CDATA. The following is an example:

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

If you don’t want to write CDATA, you need to modify the DTD constraints. You can do this in either of the following ways, but I’ll focus on the second way to rewrite code.

  • Under the classes specified location add constraints DTD files org/apache/ibatis/builder/XML/mybatis – 3 – config. The DTD to achieve the effect of covering mybatis DTD.
  • Rewrite the code to use the specified DTD.

Create CustomXMLMapperEntityResolver

Copy from the org. Apache. Ibatis. Builder. XML. XMLMapperEntityResolver, amend the MYBATIS_MAPPER_DTD to point to the local mybatis – 3 – mapper. The DTD file, And add constraints on meta elements to the DTD file.

public class CustomXMLMapperEntityResolver implements EntityResolver {· · · · · ·private static final String MYBATIS_MAPPER_DTD = "com/my/ibatis/builder/xml/mybatis-3-mapper.dtd"; ......}Copy the code
<! SQL > select * from table where table name = 'table name';
<! 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
>
Copy the code

CustomXMLLanguageDriver

Use CustomXMLMapperEntityResolver Mapper dynamic statement annotation processing.

<script>select * from user <if test= "id! =null \">where id = #{id} </if></script>" * *@paramConfiguration Mybatis Configuration *@paramScript Dynamic statement string *@paramParameterType parameterType *@return org.apache.ibatis.mapping.SqlSource
 */
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class
        parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
        // Convert the dynamic statement string to an XNode object
        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 newRawSqlSource(configuration, script, parameterType); }}}Copy the code

Create CustomXMLMapperBuilder

Copied from org. Apache. Ibatis. Builder. XML. XMLMapperBuilder, modify the constructor using CustomXMLMapperEntityResolver to parse the 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);
}
Copy the code

SqlSessionFactory

Modify the buildSqlSessionFactory method to use the CustomXMLMapperBuilder to parse the XML.

protected SqlSessionFactory buildSqlSessionFactory(a) throws Exception {· · · · · ·try {
            // Use custom 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(); }......}Copy the code

Create CustomMapperAnnotationBuilder

Copy the org. Apache. Ibatis. Builder. The annotation. MapperAnnotationBuilder, change CustomXMLMapperBuilder loadXmlResource method USES.

private void loadXmlResource(a) {
    if(! configuration.isResourceLoaded("namespace:"{+ the getName ()))......if(inputStream ! =null) {
            // Support custom tags with custom parsers
            CustomXMLMapperBuilder xmlParser = newCustomXMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); }}}Copy the code

Create CustomMapperRegistry

Copy the org. Apache. Ibatis. Binding. MapperRegistry, change CustomMapperAnnotationBuilder addMapper method USES.

@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); }}}}Copy the code

CustomConfiguration

Modify the mapperRegistry property using CustomMapperRegistry.


public class CustomConfiguration extends Configuration {· · · · · ·protected final MapperRegistry mapperRegistry = new CustomMapperRegistry(this); ......}Copy the code

Used in Spring

<! -- 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>
Copy the code

@Configuration
public class MybatisConfig {
    @Bean
    public PropertiesFactoryBean createPropertiesFactoryBean(a) 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());
        returnfactoryBean; }}Copy the code

MyBatis metadata tags generate SQL