История развития
Существующая система поддерживает набор метаданных о столбцах и ключах бизнес-таблиц и надеется автоматически инкапсулировать операторы 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;
}
}