一、SqlSource与BoundSql
1、SqlSource
- SqlSource用于描述SQL资源,MyBatis可以通过两种方式配置SQL信息,一种是通过@Select、@Insert、@Delete、@Update或者@SelectProvider、@InsertProvider、@DeleteProvider、@UpdateProvider等注解;另一种是通过XML配置文件,SqlSource就是代表Java注解或者XML文件配置的SQL资源
- 4种SqlSource实现类:
- ProviderSqlSource:描述通过@Select、@SelectProvider等注解配置的SQL资源信息
- DynamicSqlSource:描述Mapper XML文件中配置的SQL资源信息,这些SQL通常包含动态SQL配置或者${}参数占位符,需要在Mapper调用时才能确定具体的SQL语句
- RawSqlSource:描述Mapper XML文件中配置的SQL资源信息,这些语句在解析xml配置的时候就能确定,即不包含动态SQL相关配置
- StaticSqlSource:用于描述ProviderSqlSource,DynamicSqlSource,RawSqlSource解析后得到的静态SQL资源
- StaticSqlSource只封装了Mapper解析后的SQL内容和Mapper参数
2、BoundSql
- Executor组件与数据库交互时,除了需要参数映射信息外,还需要参数信息,而StaticSqlSource只封装了Mapper解析后的SQL内容和Mapper参数映射信息
- BoundSql是Executor组件执行SQL信息的封装,Executor通过BoundSql与数据库交互
public class BoundSql {
// Mapper配置解析后的sql语句
private final String sql;
// Mapper参数映射信息
private final List<ParameterMapping> parameterMappings;
// Mapper参数对象
private final Object parameterObject;
// 额外参数信息,包括<bind>标签绑定的参数,内置参数
private final Map<String, Object> additionalParameters;
// 参数对象对应的MetaObject对象
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
- BoundSql 除了封装了Mapper解析后的SQL语句和参数映射信息外,还封装了Mapper调用时传入的参数对象
- MyBatis任意一个Mapper都有两个内置的参数,即_parameter和_databaseId。_parameter代表整个参数,包括<bind>标签绑定的参数信息,这些参数存放在BoundSql 对象的additionalParameters属性中。_databaseId为Mapper配置中通过databaseId属性指定的数据库类型
二、LanguageDriver详解
- LanguageDriver用来实现SQL配置信息到SqlSource对象转换
- LanguageDriver的两个实现类
- XMLLanguageDriver为XML语言驱动,为MyBatis提供了通过XML标签结合OGNL表达式语法实现动态SQL的功能
- RawLanguageDriver仅支持静态SQL配置,不支持动态SQL功能
public class XMLLanguageDriver implements LanguageDriver {
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
// 处理XML文件中配置的SQL信息
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 解析XML文件中配置的SQL信息
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 解析sql资源
return builder.parseScriptNode();
}
// 处理Java注解中配置的SQL信息
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// 解析java注解中配置的SQL信息
// 若字符串以<script>标签开头,则以XML方式解析
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// 解析SQL配置中的全局变量
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
// 如果SQL中仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
}
三、动态Sql解析过程
XMLScriptBuilder#parseScriptNode
public SqlSource parseScriptNode() {
// 将SQL配置转换为SqlNode对象
// GO
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 判断Mapper SQL配置中是否包含动态SQL元素,如果是,就创建DynamicSqlSource对象,否则创建RawSqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
- 调用parseDynamicTags()方法将SQL配置转换为SqlNode对象,然后判断是否为动态SQL,动态SQL,创建DynamicSqlSource对象,否则创建RawSqlSource对象
- MyBatis判断是否为动态的标准是SQL配置是否包含<if>、<where>、<trim>等元素或者#{}参数占位符
XMLScriptBuilder#parseDynamicTags
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 若SQL文本中包含${}参数占位符,则为动态SQL
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
// 若SQL文本中不包含${}参数占位符,则不为动态SQL
} else {
contents.add(new StaticTextSqlNode(data));
}
// 如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
- 对SQL配置的所有子元素进行遍历,如果子元素类型为SQL文本,则使用TextSqlNode对象描述SQL节点信息,若SQL节点存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
- XMLScriptBuilder类定义了一个私有的NodeHandler接口,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,通过实现类处理对应的动态SQL标签,把动态的SQL标签转化为对应的SqlNode对象
- NodeHandler接口的实现类BindHandler、TrimHandler、WhereHandler、SetHandler、ForEachHandler、IfHandler、OtherwiseHandler、ChooseHandler
四、生成sql
- 动态SQL标签解析完成后,将解析后生成的SqlNode对象封装在SqlSource对象中
- SqlSource创建完毕后,最终会存放在MappedStatement对象的SqlSource属性中
- Executor组件操作数据库时,会调用MappedStatement#getBoundSql()方法获取BoundSql对象
MappedStatement#getBoundSql
public final class MappedStatement {
public BoundSql getBoundSql(Object parameterObject) {
// 完成SqlNode解析成Sql语句的过程
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
}
DynamicSqlSource#getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
// 通过参数对象创建动态SQL上下文对象
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 以DynamicContext对象作为参数调用apply()方法
rootSqlNode.apply(context);
// 创建SqlSourceBuilder对象
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 调用DynamicContext的getSql()方法获取动态SQL解析后的内容
// 然后调用SqlSourceBuild的parse()方法对SQL内容做进一步的处理,生成StaticSqlSource对象
// GO
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 调用StaticSqlSource对象的getBoundSql()方法获得BoundSql实例
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 将<bind>标签绑定的参数添加到BoundSql对象中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
- 根据参数对象创建DynamicContext 对象
- 调用SqlNode的apply方法对动态Sql进行解析
- context.getSql() 获取动态SQL解析后的结果
- 调用sqlSourceParser.parse方法对动态Sql解析后的结果进一步解析处理,返回StaticSqlSource
SqlSourceBuilder#parse
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// ParameterMappingTokenHandler为MyBatis参数映射处理器,用于处理#{}参数占位符
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// GenericTokenParser用于对SQL中的#{}参数占位符进行解析,获取#{}参数占位符中的内容
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
- ParameterMappingTokenHandler用于处理SQL中的#{}参数占位符
- GenericTokenParser 会对SQL中的#{}参数占位符进行解析,获取#{}参数占位符中的内容
- 例如参数占位符配置
:#{userId,javaType=long,jdbcType=NUMERIC,typeHandler=MyTypeHandler},
经过GenericTokenParser 解析后,获取参数占位符内容,即
userId,javaType=long,jdbcType=NUMERIC,typeHandler=MyTypeHandler,该对象内容会经过ParameterMappingTokenHandler对象进行替换处理
GenericTokenParser#parse
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
// 获取#{在SQL中的位置
int start = text.indexOf(openToken);
// 不存在#{占位符
if (start == -1) {
return text;
}
// 将SQL转为char数组
char[] src = text.toCharArray();
// 用于记录已解析的#{或者}的偏移量,避免重复解析
int offset = 0;
final StringBuilder builder = new StringBuilder();
// expression为#{}中的参数内容
StringBuilder expression = null;
// 遍历获取所有#{}参数占位符的内容,然后调用TokenHandler的handleToken()的方法替换参数占位符
do {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 调用TokenHandler的handleToken()方法替换参数占位符
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
- 对SQL配置中的所有#{}参数占位符进行解析,获取参数占位符的内容
- 调用ParameterMappingTokenHandler#handleToken方法对参数占位符内容进行替换
ParameterMappingTokenHandler#handleToken
@Override
public String handleToken(String content) {
// GO
parameterMappings.add(buildParameterMapping(content));
return "?";
}
- 参数占位符内容被替换成了"?"字符,因为MyBatis默认情况下会使用PreparedStatement对象与数据库交互,因此#{}被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符赋值
- buildParameterMapping()方法对占位符内容进行解析,将占位符内容转换为ParameterMapping对象
- ParameterMapping对象用于描述MyBatis参数映射消息,便于后续根据参数映射信息获取对应的TypeHandler为PreparedStatement对象设置值
SqlSourceBuilder#buildParameterMapping
private ParameterMapping buildParameterMapping(String content) {
// 将占位符内容转换为Map对象
Map<String, String> propertiesMap = parseParameterMapping(content);
// property对应的值为参数占位符名称,例如userId
String property = propertiesMap.get("property");
Class<?> propertyType;
// 如果内置参数或<bind>标签绑定的参数包含该属性,则参数类型为Getter方法返回值类型
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
// 判断该参数类型是否注册了TypeHandler,如果注册了,则使用参数类型
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
// 如果指定了jdbcType属性,并且为CURSOR类型,则使用ResultSet类型
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
// 如果参数类型为Map接口的子类型,则使用Object类型
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
// 获取parameterType对应的MetaClass对象,方便获取参数类型的反射信息
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
// 如果参数类型中包含property属性指定的内容,则使用Getter方法返回类型
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
// 使用构建者模式构建ParameterMapping对象
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
// 指定ParameterMapping对象属性
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
// 返回ParameterMapping对象
return builder.build();
}
五、#{}与${}区别
- ${}参数占位符的解析是在TextSqlNode类的apply()方法中完成的
TextSqlNode#apply
@Override
public boolean apply(DynamicContext context) {
// 通过GenericTokenParser对象解析${}参数占位符,使用BindingTokenParser对此处理参数占位符内容
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// GO
context.appendSql(parser.parse(text));
return true;
}
- BindingTokenParser对参数占位符进行替换
BindingTokenParser#handleToken
public String handleToken(String content) {
// 获取Mybatis内置参数_parameter,_parameter属性中保存所有参数信息
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
// 将参数对象添加到ContextMap对象中
context.getBindings().put("value", parameter);
}
// 通过OGNL表达式获取参数值
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
// 获取参数值
return srtValue;
}
- BindingTokenParser#handleToken会根据参数占位符名称获取对应的参数值,然后替换为对应的参数值
- ${}与#{}总结
- 使用占位符#{},占位符内容会被替换成"?"
- 而${}参数内容占位符会直接被替换为参数值
六、总结
- SqlSource用于描述MyBatis中的SQL资源信息;LanguageDriver用于解析SQL配置,将SQL配置信息转换为SqlSource对象;SqlNode用于描述动态SQL中<if>、<where>等标签信息
- LanguageDriver解析配置时,会把<if>、<where>等动态SQL标签转换为SqlNode对象,封装在SqlSource中
- 解析后的SqlSource对象会作为MappedStatement对象的属性保存在MappedStatement对象中
- 执行Mapper时,会根据传入的参数信息调用SqlSource对象的getBoundSql()方法获取BoundSql对象,这个对象完成了将SqlNode对象转换为Sql语句的过程
- ${}占位符会直接替换为传入的参数文本内容;#{}占位符会被替换为"?",然后调用JDBC中PreparedStatement对象的setXXX()方法为参数占位符设置值