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