文章目录
- 一、目标:动态SQL语句
- 二、设计:动态SQL语句
- 三、实现:动态SQL语句
- 3.0 依赖修改
- 3.1 工程结构
- 3.2 动态SQL语句类图
- 3.3 基本类型注册器
- 3.4 Ognl表达式处理
- 3.4.1 Ognl类解析器
- 3.4.2 Ognl缓存
- 3.4.3 表达式求值器
- 3.5 标签节点解析
- 3.5.1 文本SQL节点
- 3.5.2 拼装SQL节点
- 3.5.3 判断SQL节点
- 3.6 脚本构建器解析动态SQL
- 3.6.1 动态SQL源码
- 3.6.2 XML脚本构建器
- 3.7 节点处理器及其实现
- 3.7.1 节点处理器接口
- 3.7.2 节点处理器接口实现类
- 3.7.3 处理动态标签和SQL节点
- 四、测试:动态SQL语句
- 4.1 配置Mapper
- 4.2 单元测试
- 4.3 Ognl测试
- 五、总结:动态SQL语句
一、目标:动态SQL语句
💡 解析含标签的动态SQL语句?
- 之前我们在 Mapper XML 中配置的 SQL 一直都是静态的 SQL 语句,也就是说测试的是一条完整的 SQL 语句
- 例如:
select * from table where id = ?
- 例如:
- 那么在实际的使用场景中,我们往往需要根据入参对象中的字段是否有值,判断后才被设置到 SQL 语句上。
- 这样基于拼装 SQL 的方式,在 Mybatis 框架中是非常常用的。
- 所以我们需要在现有对 Mapper XML 静态 SQL 的解析上,在 XML 脚本构建器中扩充对动态 SQL 的处理,最终让我们的 ORM 框架可以配置拼装 SQL 语句。
二、设计:动态SQL语句
💡 如何设计动态SQL语句?
- 按照 XML 语句构建器,对 Mapper 文件的解析过程中。
- 包括处理
insert/delete/update/select
SQL 语句标签的参数类型、结果类型、命令类型等。 - 以及创建 SqlSource 解析 SQL 信息。
- 之前在对 SQL 信息的解析中,只做静态 SQL,不含有一些判断类型标签,如
trim
标签里的if
操作等。
- 包括处理
- 扩展 SqlSource 的构建策略,提供另外一个实现类 DynamicSqlSource 用于解析动态 SQL。
DynamicSqlSource
是一个非常常用的解析实现类。它的作用就是用于解析此类 SQL 文件。
- 首先要在 XML 语句构建器中,扩展
XMLStatementBuilder#parseStatementNode
语句解析方法中 SqlSource 的处理。- 在 SqlSource 中判断当前解析的 SQL 中是否有包含扩展的标签信息。
- 这些标签来自于 Mybatis 定义的扩展标签类型,包括:
trim/where/set/foreach/if/choose/when/otherwise/bing
。
- 解析的过程分为循环拆解脚本节点,节点类型包括:
TEXT_NODE、CDARTA_SECTION_NODE、ELEMENT_NODE
。- 而
ELEMENT_NODE
就说明当前这个节点为自定义的trim/if
等。
- 而
- 循环拆解后读取到文本和标签内容。再把这个解析出来的 SqlNode 节点结合做简单的封装,并交给动态 SQL 操作类 DynamicSqlSource 进行处理,最终返回 SqlSource 语句。
- 新增 SqlNode 节点处理的接口实现类,也都会完成
apply
接口方法的处理,完成表达式验证和填充 SQL 解析文本信息。
三、实现:动态SQL语句
3.0 依赖修改
pom.xml
<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.0.8</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
3.1 工程结构
mybatis-step-15
|-src
|-main
| |-java
| |-com.lino.mybatis
| |-annotations
| | |-Delete.java
| | |-Insert.java
| | |-Select.java
| | |-Update.java
| |-binding
| | |-MapperMethod.java
| | |-MapperProxy.java
| | |-MapperProxyFactory.java
| | |-MapperRegistry.java
| |-builder
| | |-annotations
| | | |-MapperAnnotationBuilder.java
| | |-xml
| | | |-XMLConfigBuilder.java
| | | |-XMLMapperBuilder.java
| | | |-XMLStatementBuilder.java
| | |-BaseBuilder.java
| | |-MapperBuilderAssistant.java
| | |-ParameterExpression.java
| | |-ResultMapResolver.java
| | |-SqlSourceBuilder.java
| | |-StaticSqlSource.java
| |-datasource
| | |-druid
| | | |-DruidDataSourceFacroty.java
| | |-pooled
| | | |-PooledConnection.java
| | | |-PooledDataSource.java
| | | |-PooledDataSourceFacroty.java
| | | |-PoolState.java
| | |-unpooled
| | | |-UnpooledDataSource.java
| | | |-UnpooledDataSourceFacroty.java
| | |-DataSourceFactory.java
| |-executor
| | |-keygen
| | | |-Jdbc3KeyGenerator.java
| | | |-KeyGenerator.java
| | | |-NoKeyGenerator.java
| | | |-SelectKeyGenerator.java
| | |-parameter
| | | |-ParameterHandler.java
| | |-result
| | | |-DefaultResultContext.java
| | | |-DefaultResultHandler.java
| | |-resultset
| | | |-DefaultResultSetHandler.java
| | | |-ResultSetHandler.java
| | | |-ResultSetWrapper.java
| | |-statement
| | | |-BaseStatementHandler.java
| | | |-PreparedStatementHandler.java
| | | |-SimpleStatementHandler.java
| | | |-StatementHandler.java
| | |-BaseExecutor.java
| | |-Executor.java
| | |-SimpleExecutor.java
| |-io
| | |-Resources.java
| |-mapping
| | |-BoundSql.java
| | |-Environment.java
| | |-MappedStatement.java
| | |-ParameterMapping.java
| | |-ResultFlag.java
| | |-ResultMap.java
| | |-ResultMapping.java
| | |-SqlCommandType.java
| | |-SqlSource.java
| |-parsing
| | |-GenericTokenParser.java
| | |-TokenHandler.java
| |-reflection
| | |-factory
| | | |-DefaultObjectFactory.java
| | | |-ObjectFactory.java
| | |-invoker
| | | |-GetFieldInvoker.java
| | | |-Invoker.java
| | | |-MethodInvoker.java
| | | |-SetFieldInvoker.java
| | |-property
| | | |-PropertyNamer.java
| | | |-PropertyTokenizer.java
| | |-wrapper
| | | |-BaseWrapper.java
| | | |-BeanWrapper.java
| | | |-CollectionWrapper.java
| | | |-DefaultObjectWrapperFactory.java
| | | |-MapWrapper.java
| | | |-ObjectWrapper.java
| | | |-ObjectWrapperFactory.java
| | |-MetaClass.java
| | |-MetaObject.java
| | |-Reflector.java
| | |-SystemMetaObject.java
| |-scripting
| | |-defaults
| | | |-DefaultParameterHandler.java
| | | |-RawSqlSource.java
| | |-xmltags
| | | |-DynamicContext.java
| | | |-DynamicSqlSource.java
| | | |-ExpressionEvaluator.java
| | | |-IfSqlNode.java
| | | |-MixedSqlNode.java
| | | |-OgnlCache.java
| | | |-OgnlClassResolver.java
| | | |-SqlNode.java
| | | |-StaticTextSqlNode.java
| | | |-TextSqlNode.java
| | | |-TrimSqlNode.java
| | | |-XMLLanguageDriver.java
| | | |-XMLScriptBuilder.java
| | |-LanguageDriver.java
| | |-LanguageDriverRegistry.java
| |-session
| | |-defaults
| | | |-DefaultSqlSession.java
| | | |-DefaultSqlSessionFactory.java
| | |-Configuration.java
| | |-ResultContext.java
| | |-ResultHandler.java
| | |-RowBounds.java
| | |-SqlSession.java
| | |-SqlSessionFactory.java
| | |-SqlSessionFactoryBuilder.java
| | |-TransactionIsolationLevel.java
| |-transaction
| | |-jdbc
| | | |-JdbcTransaction.java
| | | |-JdbcTransactionFactory.java
| | |-Transaction.java
| | |-TransactionFactory.java
| |-type
| | |-BaseTypeHandler.java
| | |-DateTypeHandler.java
| | |-IntegerTypeHandler.java
| | |-JdbcType.java
| | |-LongTypeHandler.java
| | |-SimpleTypeRegistry.java
| | |-StringTypeHandler.java
| | |-TypeAliasRegistry.java
| | |-TypeHandler.java
| | |-TypeHandlerRegistry.java
|-test
|-java
| |-com.lino.mybatis.test
| |-dao
| | |-IActivityDao.java
| |-po
| | |-Activity.java
| |-ApiTest.java
|-resources
|-mapper
| |-Activity_Mapper.xml
|-mybatis-config-datasource.xml
3.2 动态SQL语句类图
- 通过 XML 语句驱动器 XMLLanguageDriver,创建的 XML 脚本构建器解析 XMLScriptBuilder 为入口,开始执行脚本的解析操作。
- 在整个解析的过程中,扩展了 SqlSource 的动态 SQL 解析 DynamicSqlSource、新增了 SqlNode 节点处理,以及对应的 NodeHandler 节点处理器。
- 而关于整个 ORM 框架的执行流程,从解析、存放、执行、返回等。
- 其实处理扩展了这部分对动态 SQL 的处理,其余的流程不需要做任何扩展。
- 这里体现了框架结构关于解耦设计的重要性。
3.3 基本类型注册器
SimpleTypeRegistry.java
package com.lino.mybatis.type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
* @description: 基本类型注册器
*/
public class SimpleTypeRegistry {
private static final Set<Class<?>> SIMPLE_TYPE_SET = new HashSet<>();
static {
SIMPLE_TYPE_SET.add(String.class);
SIMPLE_TYPE_SET.add(Byte.class);
SIMPLE_TYPE_SET.add(Short.class);
SIMPLE_TYPE_SET.add(Character.class);
SIMPLE_TYPE_SET.add(Integer.class);
SIMPLE_TYPE_SET.add(Long.class);
SIMPLE_TYPE_SET.add(Float.class);
SIMPLE_TYPE_SET.add(Double.class);
SIMPLE_TYPE_SET.add(Boolean.class);
SIMPLE_TYPE_SET.add(Date.class);
SIMPLE_TYPE_SET.add(Class.class);
SIMPLE_TYPE_SET.add(BigInteger.class);
SIMPLE_TYPE_SET.add(BigDecimal.class);
}
public SimpleTypeRegistry() {
}
/**
* 是否是基本类型
*
* @param clazz 类型
* @return 是否
*/
public static boolean isSimpleType(Class<?> clazz) {
return SIMPLE_TYPE_SET.contains(clazz);
}
}
3.4 Ognl表达式处理
3.4.1 Ognl类解析器
OgnlClassResolver.java
package com.lino.mybatis.scripting.xmltags;
import com.lino.mybatis.io.Resources;
import ognl.ClassResolver;
import java.util.HashMap;
import java.util.Map;
/**
* @description: Ognl类解析器
*/
public class OgnlClassResolver implements ClassResolver {
private Map<String, Class<?>> classes = new HashMap<>(101);
@Override
public Class classForName(String className, Map context) throws ClassNotFoundException {
Class<?> result = null;
if ((result = classes.get(className)) == null) {
try {
result = Resources.classForName(className);
} catch (ClassNotFoundException e1) {
if (className.indexOf('.') == -1) {
result = Resources.classForName("java.lang" + className);
classes.put("java.lang." + className, result);
}
}
classes.put(className, result);
}
return result;
}
}
3.4.2 Ognl缓存
OgnlCache.java
package com.lino.mybatis.scripting.xmltags;
import ognl.Ognl;
import ognl.OgnlException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description: OGNL缓存 http://code.google.com/p/mybatis/issues/detail?id=342
* OGNL 是 Object-Graph Navigation Language 的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL)
* 通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。
* 它使用相同的表达式去存取对象的属性。
*/
public class OgnlCache {
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();
public OgnlCache() {
}
public static Object getValue(String expression, Object root) {
try {
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new RuntimeException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
// OgnlParser.topLevelExpression 操作耗时,加个缓存放到 ConcurrentHashMap 里面
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}
}
3.4.3 表达式求值器
ExpressionEvaluator.java
package com.lino.mybatis.scripting.xmltags;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @description: 表达式求值器
*/
public class ExpressionEvaluator {
/**
* 表达式求布尔值,比如 username == 'xiaoling'
*
* @param expression 表达式
* @param parameterObject 参数对象
* @return 布尔值
*/
public boolean evaluateBoolean(String expression, Object parameterObject) {
// 非常简单,就是调用ognl
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
// 如果是Boolean
return (Boolean) value;
}
if (value instanceof Number) {
// 如果是Number,判断不为0
return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
}
// 否则判断不为null
return value != null;
}
public Iterable<?> evaluateIterator(String expression, Object parameterObject) {
// 原生的ognl很强大,OgnlCache.getValue直接就可以返回一个Iterable型或数组型或Map型
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new RuntimeException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) {
return (Iterable<?>) value;
}
if (value.getClass().isArray()) {
// 如果是array,则把他变成一个List<Object>
// 注释下面提到了,不能用Arrays.asList(),因为array可能是基本型,这样会出ClassCastException,
// 见https://code.google.com/p/mybatis/issues/detail?id=209
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List<Object> answer = new ArrayList<>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
if (value instanceof Map) {
return ((Map) value).entrySet();
}
throw new RuntimeException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}
}
- 根据
expression
获取对应的结果值,其实核心调用的是Ognl.parseExpression(expression)
。- 不过此方法有些耗时,因此被加入到缓存提供服务。
- 这个方法会根据你的表达式和入参信息,判断是否满足条件。
- 例如:
null != activityId
那么就会判断parameterObject
的入参是否包含了activityId
对应的属性值。
- 例如:
3.5 标签节点解析
- 因为有了动态 SQL 的处理,相当于在 SQL 语句中包含:
test文本、trim拼装、if判断
,所以为了解决这一差异化的解析,需要提供不同的处理策略。- 为此需要实现已有的 StaticTextSqlNode 静态文本 SQL 节点所实现的接口。
- 新增
TextSqlNode
、TrimSqlNode
、IfSqlNode
这些实现类。
3.5.1 文本SQL节点
TextSqlNode.java
package com.lino.mybatis.scripting.xmltags;
import com.lino.mybatis.parsing.GenericTokenParser;
import com.lino.mybatis.parsing.TokenHandler;
import com.lino.mybatis.type.SimpleTypeRegistry;
import java.util.regex.Pattern;
/**
* @description: 文本SQL节点(CDATA | TEXT)
*/
public class TextSqlNode implements SqlNode {
private String text;
private Pattern injectionFilter;
public TextSqlNode(String text) {
this(text, null);
}
public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
/**
* 判断是否是动态sql
*/
public boolean isDynamic() {
DynamicCheckTokenParser checker = new DynamicCheckTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
/**
* 绑定记号解析器
*/
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
// 从缓存里取得值
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));
checkInjection(srtValue);
return srtValue;
}
/**
* 检查是否匹配正则表达式
*
* @param value 值
*/
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new RuntimeException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
/**
* 动态SQL检查器
*/
private static class DynamicCheckTokenParser implements TokenHandler {
private boolean isDynamic;
public DynamicCheckTokenParser() {
}
public boolean isDynamic() {
return isDynamic;
}
@Override
public String handleToken(String content) {
// 设置 isDynamic 为 true,即调用了这个类就必定是动态SQL
this.isDynamic = true;
return null;
}
}
}
TextSqlNode#apply
方法用于解析 CDATA | TEXT 的 SQL 节点,使用GenericTokenParser
记号解析器进行处理操作。- 这部分用到 OGNL 表达式,这个工具组件可以帮助我们非常方便的获取对象的属性值信息。
- 例如:
Ognl.getValue("属性名称", context, root)
- 例如:
3.5.2 拼装SQL节点
TrimSqlNode.java
package com.lino.mybatis.scripting.xmltags;
import com.lino.mybatis.session.Configuration;
import java.util.*;
/**
* @description: trim Sql Node 节点解析
*/
public class TrimSqlNode implements SqlNode {
private SqlNode contents;
private String prefix;
private String suffix;
private List<String> prefixesToOverride;
private List<String> suffixesToOverride;
private Configuration configuration;
public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}
protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}
@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
}
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}
@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
@Override
public String getSql() {
return delegate.getSql();
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}
}
TrimSqlNode
节点的解析,主要依赖于 FilteredDynamicContext 对配置信息的拼装,把 AND | OR 等,拼装到 SQL 语句上进行返回处理。
3.5.3 判断SQL节点
IfSqlNode.java
package com.lino.mybatis.scripting.xmltags;
/**
* @description: IF SQL 节点
*/
public class IfSqlNode implements SqlNode {
private ExpressionEvaluator evaluator;
private String test;
private SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
@Override
public boolean apply(DynamicContext context) {
// 如果满足条件,则apply,并返回true
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
IfSqlNode
的标签处理,主要是验证标签表达式是否满足要求。而这个判断操作就是使用 OGNL 表达式进行处理。
3.6 脚本构建器解析动态SQL
- 所有的 XML 的 SQL 解析操作都会进入到 XMLScriptBuider 脚本构建器中进行处理。
- 扩展:循环检测
insert/delete/update/select
标签传递进来的 SQL,基于传递进来的 SQL 语句检测是否包含了额外的标签。- 标签:
trim/where/set/foreach/if/choose/when/otherwise/bing
。
- 标签:
3.6.1 动态SQL源码
DynamicSqlSource.java
package com.lino.mybatis.scripting.xmltags;
import com.lino.mybatis.builder.SqlSourceBuilder;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.session.Configuration;
import java.util.Map;
/**
* @description: 动态SQL源码
*/
public class DynamicSqlSource implements SqlSource {
private Configuration configuration;
private SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 生成一个 DynamicContext 动态上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
// SqlNode.apply 将 ${} 参数替换掉,不替换 #{} 这种参数
rootSqlNode.apply(context);
// 调用 SqlSourceBuilder
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// SqlSourceBuilder.parse 这里返回的是 StaticSqlSource,解析过程就把那些参数都替换成?了,也就是最基本的JDBC的SQL语句
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// SqlSource.getBoundSql,非递归调用,而是调用 StaticSqlSource 实现类
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
- 在
DynamicSqlSource#getBoundSql
的接口方法中,包括:生成解析元素的动态上下文、替换占位符、通过 SqlSourceBuilder SQL 源码构建器,提供的parse
解析方法,返回静态 SQL 语句。- 其实最后异步就相当于把即使是动态 SQL 也归为静态的 SQL 语句来处理。
- 最后基于返回的 SqlSource 以及参数信息,构建出 BoundSql 把相关的参数类型、返回类型等信息填充。
- 另外这里
rootSqlNode.apply(context)
,对trim、if
等标签的内容会调用到具体的 SqlNode 节点实现类中。
3.6.2 XML脚本构建器
XMLScriptBuilder.java
package com.lino.mybatis.scripting.xmltags;
import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.defaults.RawSqlSource;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;
import org.dom4j.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @description: XML脚本构建器
*/
public class XMLScriptBuilder extends BaseBuilder {
private Element element;
private boolean isDynamic;
private Class<?> parameterType;
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
public XMLScriptBuilder(Configuration configuration, Element element, Class<?> parameterType) {
super(configuration);
this.element = element;
this.parameterType = parameterType;
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
// 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bind
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("if", new IfHandler());
}
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(element);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
private List<SqlNode> parseDynamicTags(Element element) {
List<SqlNode> contents = new ArrayList<>();
List<Node> children = element.content();
for (Node child : children) {
if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {
String data = child.getText();
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new RuntimeException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(element.element(child.getName()), contents);
isDynamic = true;
}
}
return contents;
}
private interface NodeHandler {
void handleNode(Element nodeToHandle, List<SqlNode> targetContents);
}
private class TrimHandler implements NodeHandler {
@Override
public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String prefix = nodeToHandle.attributeValue("prefix");
String prefixOverrides = nodeToHandle.attributeValue("prefixOverrides");
String suffix = nodeToHandle.attributeValue("suffix");
String suffixOverrides = nodeToHandle.attributeValue("suffixOverrides");
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
private class IfHandler implements NodeHandler {
@Override
public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.attributeValue("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
}
3.7 节点处理器及其实现
3.7.1 节点处理器接口
NodeHandler.java
- 定义节点处理器接口,所有的标签处理也都是基于这个接口来实现的
private interface NodeHandler {
void handleNode(Element nodeToHandle, List<SqlNode> targetContents);
}
3.7.2 节点处理器接口实现类
TrimHandler.java
private class TrimHandler implements NodeHandler {
@Override
public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String prefix = nodeToHandle.attributeValue("prefix");
String prefixOverrides = nodeToHandle.attributeValue("prefixOverrides");
String suffix = nodeToHandle.attributeValue("suffix");
String suffixOverrides = nodeToHandle.attributeValue("suffixOverrides");
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
private class IfHandler implements NodeHandler {
@Override
public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.attributeValue("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
<trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">...</trim>
- 解析
trim
标签信息,把字段prefix、prefixOverrides、suffixOverrides
都依次获取出来,使用 TrimSqlNode 构建后存放到List<SqlNode>
中。 <if test="null != activityId">...</if>
- 解析
if
语句标签,与解析trim
标签类似,获取标签配置test
语句表达式,使用 IfSqlNode 进行构建,构建后存放到List<SqlNode>
中。
3.7.3 处理动态标签和SQL节点
private void initNodeHandlerMap() {
// 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bind
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("if", new IfHandler());
}
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(element);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
private List<SqlNode> parseDynamicTags(Element element) {
List<SqlNode> contents = new ArrayList<>();
List<Node> children = element.content();
for (Node child : children) {
if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {
String data = child.getText();
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new RuntimeException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(element.element(child.getName()), contents);
isDynamic = true;
}
}
return contents;
}
- 在
initNodeHandlerMap
中初始化trim、if
标签处理。 - 在
parseDynamicTags
中处理解析动态 SQL、静态 SQL 前,需要对parseDynamicTags
进行扩展。- 因为这里已经在 SQL 标签里配置了拼装 SQL 类标签,所以解析的过程会变得复杂一些。
- 在解析过程中,先获取当前
Element
元素下的 Node 节点结合,再根据这些节点的类型,TEXT_NODE、CDATA_SECTION_NODE、ELEMENT_NODE
进行不同的解析处理。 - 当循环处理中遇到了新的
Element
元素,则从nodeHandlerMap
查找到对应的标签处理器进行处理。 - 注意:Mybatis 源码采用的是
w3c dom
解析处理。
- 在
parseScriptNode
中关于动态 SQL 和 静态 SQL 的解析处理,根据isDynamic
判断选择不同的解析方式,并最终返回 SqlSource 信息。
四、测试:动态SQL语句
4.1 配置Mapper
Activity_Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lino.mybatis.test.dao.IActivityDao">
<resultMap id="activityMap" type="com.lino.mybatis.test.po.Activity">
<id column="id" property="id"/>
<result column="activity_id" property="activityId"/>
<result column="activity_name" property="activityName"/>
<result column="activity_desc" property="activityDesc"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<select id="queryActivityById" parameterType="com.lino.mybatis.test.po.Activity" resultMap="activityMap">
SELECT activity_id, activity_name, activity_desc, create_time, update_time
FROM activity
<trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
<if test="null != activityId">
activity_id = #{activityId}
</if>
</trim>
</select>
</mapper>
- 在查询语句的配置中,新增加了
trim、if
用于拼接 SQL 标签。
4.2 单元测试
ApiTest.java
@Test
public void test_queryActivityById() {
// 1.获取映射器对象
IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
// 2.测试验证
Activity activity = new Activity();
activity.setActivityId(100001L);
Activity result = dao.queryActivityById(activity);
logger.info("测试结果:{}", JSON.toJSONString(result));
}
测试结果
10:55:40.554 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 1202683709.
10:55:40.569 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
10:55:40.569 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}
- 根据在 XMLScriptBuilder 脚本构建器中添加断点调试,以及输出结果,可以验证实现的标签拼装 SQL 语句,可以正常运行。
4.3 Ognl测试
ApiTest.java
@Test
public void test_ognl() throws OgnlException {
Activity activity = new Activity();
activity.setActivityId(1L);
activity.setActivityName("测试活动");
activity.setActivityDesc("测试内容");
OgnlContext context = new OgnlContext();
context.setRoot(activity);
Object root = context.getRoot();
Object activityName = Ognl.getValue("activityName", context, root);
Object activityDesc = Ognl.getValue("activityDesc", context, root);
Object value = Ognl.getValue("activityDesc.length()", context, root);
System.out.println(activityName + "\t" + activityDesc + " length:" + value);
}
测试结果
测试活动 测试内容 length:4
五、总结:动态SQL语句
- 扩充解析策略。首先是对静态SQL的解析扩展到对动态SQL解析的操作,之后对于动态SQL中含有的标签,提供不同的解析策略。
trim、if
,对于一些其他的标签where、set、foreach
等。