文章目录
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>
from
t_user
where
id = #{id}
</select>
<select id="selectUserList" resultMap="BaseResultMap">
select
<include refid="baseSQL"></include>
from
t_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()方法。
@Override
public 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 reading
return 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;
}
@Override
public 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;
}
@Override
public 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();
}
@Override
public 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;
}
@Override
public 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 update
String 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中的占位符 ParameterMapping
SqlSource 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中的占位符 ParameterMapping
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
3. 获取真正执行的sql
在CachingExecutor.query()生成真正待执行的sql。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取SQL
BoundSql 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) {
// 拼接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;
}
总结:通过SqlSource、DynamicContext和SqlNode的精妙配合,获得最终执行的sql。