mybatis如何生成和执行动态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>
        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。

  1. 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);
      }
    }
  1. 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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值