文章目录
- 1. 相关代码
- 2. SQL 语句解析流程
- 2.1 XMLStatementBuilder
- 2.2 SqlSource
- 2.3 DynamicContext上下文
- 2.4 SqlNode和组合模式
- 2.5 MappedStatement
- 2.6 解析标签
- 2.6.1 \<include>
- 2.6.2 \<selectKey>
- 2.6.3 处理 SQL 语句
- 3. 获取真正执行的sql
1. 相关代码
package com.boge.mapper;import com.boge.pojo.User;import java.util.List;public interface UserMapper {List<User> selectUserList(User user);User selectUserById(Integer id);int updateById(User user);
}
<?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.boge.mapper.UserMapper"><cache/><resultMap id="BaseResultMap" type="com.boge.pojo.User"><id property="id" column="id" jdbcType="INTEGER"/><result property="userName" column="user_name" jdbcType="VARCHAR"/><result property="realName" column="real_name" jdbcType="VARCHAR"/><result property="password" column="password" jdbcType="VARCHAR"/><result property="age" column="age" jdbcType="INTEGER"/><result property="dId" column="d_id" jdbcType="INTEGER"/></resultMap><sql id="baseSQL">id,user_name,real_name,password,age,d_id</sql><select id="selectUserById" resultType="com.boge.pojo.User">select<include refid="baseSQL"></include>fromt_userwhereid = #{id}</select><select id="selectUserList" resultMap="BaseResultMap">select<include refid="baseSQL"></include>fromt_user t<where><if test="userName != null and userName.trim() != ''">and t.user_name like concat('%', #{userName},'%')</if><if test="age != null">and t.age = {age}</if></where></select><update id="updateById">update t_user set user_name = #{userName} where id = #{id}</update></mapper>
2. SQL 语句解析流程
2.1 XMLStatementBuilder
- XMLStatementBuilder。映射文件由<select>、<insert>、<delete>、<update>等标签是由XMLStatementBuilder.parseStatementNode()进行解析,不在由XMLMapperBuilder解析。
2.2 SqlSource
用来表示解析之后的sql语句。只定义一个方法getBoundSql() ,根据解析到的sql语句和入参生成一条可执行的sql。
public interface SqlSource {BoundSql getBoundSql(Object parameterObject);
}
核心实现:
核心类介绍:
- DynamicSqlSource,当 SQL 语句中包含动态 SQL 和“${}”占位符的时候,会使用 DynamicSqlSource 对象。
判断一个 SQL 片段是否为动态 SQL,判断的标准是:如果这个 SQL 片段包含了未解析的“${}”占位符或动态 SQL 标签,则为动态 SQL 语句。但注意,如果是只包含了“#{}”占位符,也不是动态 SQL。
-
RawSqlSource,DynamicSqlSource 有两个不同之处:
- RawSqlSource 处理的是非动态 SQL 语句,DynamicSqlSource 处理的是动态 SQL 语句;
- RawSqlSource 解析 SQL 语句的时机是在初始化流程中,而 DynamicSqlSource 解析动态 SQL 的时机是在程序运行过程中,也就是运行时解析。
-
StaticSqlSource, DynamicSqlSource 还是 RawSqlSource,底层都依赖 SqlSourceBuilder 解析之后得到的 StaticSqlSource 对象。StaticSqlSource 中维护了解析之后的 SQL 语句以及“#{}”占位符的属性信息(List 集合),其 getBoundSql() 方法是真正创建 BoundSql 对象的地方,这个 BoundSql 对象包含了上述 StaticSqlSource 的两个字段以及实参的信息。
2.3 DynamicContext上下文
在 MyBatis 解析一条动态 SQL 语句的时候,可能整个流程非常长,其中涉及多层方法的调用、方法的递归、复杂的循环等,其中产生的中间结果需要有一个地方进行存储,那就是 DynamicContext 上下文对象。
DynamicContext 的两个核心属性:
- sqlBuilder 字段(StringJoiner 类型),用来记录解析之后的 SQL 语句(拼接sql);
- bindings 字段,用来记录上下文中的一些 KV 信息(其实就是方法入参)。
DynamicContext定义了内部类:ContextMap,ContextAccessor。
- ContextMap
ContextMap继承HashMap,覆写了get()方法。
@Overridepublic Object get(Object key) {String strKey = (String) key;if (super.containsKey(strKey)) {return super.get(strKey);}if (parameterMetaObject == null) {return null;}if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {return parameterMetaObject.getOriginalObject();} else {// issue #61 do not modify the context when readingreturn parameterMetaObject.getValue(strKey);}}
- ContextAccessor
DynamicContext 定义了一个静态代码块,指定了 OGNL 表达式读写 ContextMap 集合的逻辑,这部分读取逻辑封装在 ContextAccessor 中。除此之外,你还可以看到 ContextAccessor 中的 getProperty() 方法会将传入的 target 参数(实际上就是 ContextMap)转换为 Map,并先尝试按照 Map 规则进行查找;查找失败之后,会尝试获取“_parameter”对应的 parameterObject 对象,从 parameterObject 中获取指定的 Value 值。
2.4 SqlNode和组合模式
在 MyBatis 处理动态 SQL 语句的时候,会将动态 SQL 标签解析为 SqlNode 对象,多个 SqlNode 对象就是通过组合模式组成树形结构供上层使用的。
接口定义:
public interface SqlNode {// apply()方法会根据用户传入的实参,解析该SqlNode所表示的动态SQL内容并// 将解析之后的SQL片段追加到DynamicContext.sqlBuilder字段中暂存。// 当SQL语句中全部的动态SQL片段都解析完成之后,就可以从DynamicContext.sqlBuilder字段中// 得到一条完整的、可用的SQL语句了boolean apply(DynamicContext context);}
1. StaticTextSqlNode 和 MixedSqlNode
StaticTextSqlNode 用于表示非动态的 SQL 片段,其中维护了一个 text 字段(String 类型),用于记录非动态 SQL 片段的文本内容,其 apply() 方法会直接将 text 字段值追加到 DynamicContext.sqlBuilder 的最末尾。
/*** @author Clinton Begin*/
public class StaticTextSqlNode implements SqlNode {private final String text;public StaticTextSqlNode(String text) {this.text = text;}@Overridepublic boolean apply(DynamicContext context) {context.appendSql(text);return true;}}
MixedSqlNode 在整个 SqlNode 树中充当树枝节点,也就是扮演组合模式中 Composite 的角色,其中维护了一个 List 集合用于记录 MixedSqlNode 下所有的子 SqlNode 对象。MixedSqlNode 对于 apply() 方法的实现也相对比较简单,核心逻辑就是遍历 List 集合中全部的子 SqlNode 对象并调用 apply() 方法,由子 SqlNode 对象完成真正的动态 SQL 处理逻辑。
/*** @author Clinton Begin*/
public class MixedSqlNode implements SqlNode {private final List<SqlNode> contents;public MixedSqlNode(List<SqlNode> contents) {this.contents = contents;}@Overridepublic boolean apply(DynamicContext context) {contents.forEach(node -> node.apply(context));return true;}
}
2. TextSqlNode
TextSqlNode 实现处理包含 “${}”占位符的动态 SQL 片段,将占位符替换为参数。
3. IfSqlNode
IfSqlNode 实现类处理动态 SQL 语句 <if>标签, <if>标签的test表达式,通过ExpressionEvaluator判断为真,继续执行子SqlNode 对象。ExpressionEvaluator的底层实现就是OGNL。
/*** @author Clinton Begin*/
public class IfSqlNode implements SqlNode {private final ExpressionEvaluator evaluator;private final String test;private final SqlNode contents;public IfSqlNode(SqlNode contents, String test) {this.test = test;this.contents = contents;this.evaluator = new ExpressionEvaluator();}@Overridepublic boolean apply(DynamicContext context) {if (evaluator.evaluateBoolean(test, context.getBindings())) {contents.apply(context);return true;}return false;}}
4. TrimSqlNode
TrimSqlNode 处理 MyBatis 动态 SQL 语句中的 <trim> 标签。
指定 prefix 和 suffix 属性添加前缀和后缀,也可以指定 prefixesToOverrides 和 suffixesToOverrides 属性来删除多个前缀和后缀(使用“|”分割不同字符串)
public class TrimSqlNode implements SqlNode {private final SqlNode contents;private final String prefix;private final String suffix;private final List<String> prefixesToOverride;private final List<String> suffixesToOverride;private final 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;}@Overridepublic boolean apply(DynamicContext context) {FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);boolean result = contents.apply(filteredDynamicContext);filteredDynamicContext.applyAll();return result;}...
}
5. WhereSqlNode和SetSqlNode
两者都继承了TrimSqlNode ,可以是处理特定类型的TrimSqlNode 。
public class WhereSqlNode extends TrimSqlNode {private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");public WhereSqlNode(Configuration configuration, SqlNode contents) {super(configuration, contents, "WHERE", prefixList, null, null);}}
/*** @author Clinton Begin*/
public class SetSqlNode extends TrimSqlNode {private static final List<String> COMMA = Collections.singletonList(",");public SetSqlNode(Configuration configuration,SqlNode contents) {super(configuration, contents, "SET", COMMA, null, COMMA);}}
6. ForEachSqlNode
处理<foreach>标签。
2.5 MappedStatement
用来表示解析之后的sql标签,包含sqlSource和sqlCommandType,分别记录了SQL 标签中定义的 SQL 语句和 SQL 语句的类型(INSERT、UPDATE、DELETE、SELECT 或 FLUSH 类型)。
public void parseStatementNode() {// 获取SQL标签的id以及databaseId属性String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");// 若databaseId属性值与当前使用的数据库不匹配,则不加载该SQL标签// 若存在相同id且databaseId不为空的SQL标签,则不再加载该SQL标签if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 获取节点的名称 select insert delete updateString nodeName = context.getNode().getNodeName();// 获取到具体的 sql 命令 类型SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));boolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);boolean useCache = context.getBooleanAttribute("useCache", isSelect);boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// Include Fragments before parsing 解析 include标签 替换include 标签 完成 ${} 解析XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);// Parse selectKey after includes and remove them.processSelectKeyNodes(id, parameterTypeClass, langDriver);// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}// 动态SQL的加载解析 同时记录了 sql中的占位符 ParameterMappingSqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);String resultMap = context.getStringAttribute("resultMap");String resultSetType = context.getStringAttribute("resultSetType");ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);if (resultSetTypeEnum == null) {resultSetTypeEnum = configuration.getDefaultResultSetType();}String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");String resultSets = context.getStringAttribute("resultSets");// >> 关键的一步: MappedStatement 的创建builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);}
2.6 解析标签
2.6.1 <include>
<include>作用是引入由<sql>标签定义的sql片段,<sql>实际在XMLMapperBuilder.sqlElement()解析。
<include> 标签由XMLIncludeTransformer.applyIncludes()处理,同时还会处理<include> 标签下的<property>标签和“${}"占位符。
2.6.2 <selectKey>
略。
2.6.3 处理 SQL 语句
// 动态SQL的加载解析 同时记录了 sql中的占位符 ParameterMappingSqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
3. 获取真正执行的sql
在CachingExecutor.query()生成真正待执行的sql。
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获取SQLBoundSql boundSql = ms.getBoundSql(parameterObject);// 创建CacheKey:什么样的SQL是同一条SQL? >>// select * from t_user select id,username,password form t_user// 根据特定的规则生成一个key 保证不冲突CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
进入getBoundSql(),
public BoundSql getBoundSql(Object parameterObject) {// 拼接sqlBoundSql 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;}
总结:通过SqlSource、DynamicContext和SqlNode的精妙配合,获得最终执行的sql。