Mybatis 动态SQL实现

Mybatis 动态SQL实现

动态SQL介绍

动态SQL是Mybatis的强大特性之一,可以简化根据不同条件使用不同SQL的操作,并自动去除不需要的关键字。在Mybatis3中引入了OGNL表达式,使学习元素种类的过程变得简易。

动态SQL使用

if 元素

<select id="findActivePersonLike" resultType="Person">
  SELECT * FROM Person WHERE state = ‘ACTIVE’
  <if test="name != null">
    AND name like #{name}
  if>
  <if test="age != null">
    AND age = #{age}
  if>
select>

choose、when、otherwise 元素

<select id="findActivePersonLike" resultType="Person">
  SELECT * FROM Person WHERE state = ‘ACTIVE’
  <choose>
    <when test="name != null">
      AND name like #{name}
    when>
    <when test="age != null">
      AND age = #{age}
    when>
    <otherwise>
      AND phone_number != null
    otherwise>
  choose>
select>

trim、where、set 元素

<select id="findActivePersonLike" resultType="Person">
  SELECT * FROM Person
  <where>
    <if test="name != null">
         name = #{name}
    if>
    <if test="age != null">
        AND age = #{age}
    if>
    <if test="birthdayYear != null">
        AND YEAR(birthday) = #{birthdayYear}
    if>
  where>
select>

where元素只会在子元素返回任何内容的情况下才插入“WHERE”子句。而且,若子句的开头为“AND”或“OR”,where元素也会将它们去除。

如where元素无法满足要求,可以用trim定制where元素的功能,使之更加灵活。

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
trim>

以上trim元素的定义,等同于where元素的效果。prefixOverrides属性会忽略通过管道符分隔的文本序列,该属性中的值会被移除,并且插入prefix属性中指定的内容。

set语句用于动态生成数据库更新SQL,如下:

<update id="updatePersonIfNecessary">
  update Person
    <set>
      <if test="name != null">name=#{name},if>
      <if test="age != null">age=#{age},if>
      <if test="phoneNumber != null">phone_number=#{phoneNumber},if>
      <if test="birthday != null">birthday=#{birthday}if>
    set>
  where id=#{id}
update>

set元素会动态在首行加入set关键字,并去除if标签不满足要求时的尾部逗号。同样,set元素也可以被trim元素等价替换,如下:

<trim prefix="SET" suffixOverrides=",">
  ...
trim>

foreach 元素

<select id="selectPersonNameIn" resultType="Person">
  SELECT * FROM Person
  <where>
    <foreach item="item" index="index" collection="list"
        open="name in (" separator="," close=")" nullable="true">
          #{item}
    foreach>
  where>
select>

foreach元素中集合参数支持任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数,当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

bind 元素

bind 元素允许你在OGNL表达式以外创建一个变量,并将其绑定到当前的上下文。如:

<select id="selectPersonsLike" resultType="Person">
  <bind name="pattern" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM Person
  WHERE name LIKE #{pattern}
select>

script 元素

script 元素用于在带注解的映射器接口类中使用动态 SQL。

    @Update({""})
    void updatePersonValues(Person person);

动态SQL源码分析

在项目中Spring和Mybatis整合时,需要在xml配置文件中配置如下类似内容:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="multipleDataSource"/>
		<property name="configLocation">
			<value>classpath:sqlMapConfig.xmlvalue>
		property>
bean>

上述配置中引入了org.mybatis.spring.SqlSessionFactoryBean类,通过如下源码可知:

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>

该类继承了FactoryBean(工厂类接口)、InitializingBean(为bean提供属性初始化后的处理方法)及ApplicationListener(观察者设计模式中监听作用),主要关注初始化bean的方式,即InitializingBean中afterPropertiesSet()方法的实现。

 @Override
 public void afterPropertiesSet() throws Exception {
   notNull(dataSource, "Property 'dataSource' is required");
   notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
   state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
             "Property 'configuration' and 'configLocation' can not specified with together");

   this.sqlSessionFactory = buildSqlSessionFactory();
 }

该方法除了进行一些数据的判断外,主要是调用了buildSqlSessionFactory()方法,用于返回SqlSessionFactory实例。buildSqlSessionFactory()方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。部分源码如下:

if (this.configLocation != null) {
     xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
     configuration = xmlConfigBuilder.getConfiguration();
}
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
            configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();

因XMLConfigBuilder最后会调用parse()方法,且方法内部解析mappers的方法mapperElement(root.evalNode("mappers"))最终还是使用XMLMapperBuilder类中configurationElement(XNode context)方法进行操作,所以直接关注该方法的操作即可。

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

此方法主要是处理缓存、查询参数、返回结果、sql语句及增删改查节点。本次主要关注的是增删改查节点的处理,如下:

  private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

和动态SQL相关的源码如下:

    // Parse the SQL (pre:  and  were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

其中,通过xml构建SQL的实现类是XMLLanguageDriver。

  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

可见,动态SQL的生成主要是通过XMLScriptBuilder类实现的。其中对于动态SQL语句脚本节点的生成源码如下:

  public SqlSource parseScriptNode() {
    List<SqlNode> contents = parseDynamicTags(context);
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

  List<SqlNode> parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlers(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return contents;
  }

XMLScriptBuilder类为每一个标签关键字创建了一个Handler,如下:

  NodeHandler nodeHandlers(String nodeName) {
    Map<String, NodeHandler> map = new HashMap<String, NodeHandler>();
    map.put("trim", new TrimHandler());
    map.put("where", new WhereHandler());
    map.put("set", new SetHandler());
    map.put("foreach", new ForEachHandler());
    map.put("if", new IfHandler());
    map.put("choose", new ChooseHandler());
    map.put("when", new IfHandler());
    map.put("otherwise", new OtherwiseHandler());
    map.put("bind", new BindHandler());
    return map.get(nodeName);
  }

以if为例,查看IfHandler()。

  private class IfHandler implements NodeHandler {
    public IfHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

在此方法中创建了IfSqlNode对象,后由该对象中的apply(DynamicContext context)方法处理SQL的动态拼接,如下:

  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

  public boolean evaluateBoolean(String expression, Object parameterObject) {
    Object value = OgnlCache.getValue(expression, parameterObject);
    if (value instanceof Boolean) {
      return (Boolean) value;
    }
    if (value instanceof Number) {
        return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
    }
    return value != null;
  }

在调用链的最后,动态SQL的拼接基于的是OGNL表达式的判断结果,与Mybatis官方文档中内容一致。

OGNL表达式的使用

OGNL (Object Graph Navigation Language) 是一个开源的表达式引擎。通过使用OGNL,我们能够通过表达式存取Java对象树中的任意属性和调用Java对象树的方法等。

OGNL所在类的源码如下:

public abstract class Ognl {
  
  /**
   * 通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值 
   */
  public static Object getValue(String expression, Map context, Object root) throws OgnlException {
      return getValue(expression, context, root, null);
  }

  /**
   * 通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值 
   */
  public static void setValue(String expression, Map context, Object root, Object value) throws OgnlException {
      setValue(parseExpression(expression), context, root, value);
  }

}

OGNL三要素:

表达式(Expression)

表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。因此,表达式其实是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。

Root对象(Root Object)

OGNL的Root对象可以理解为OGNL的操作对象。当OGNL表达式规定了“干什么”以后,我们还需要指定对谁干。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,如果我们有一个OGNL的表达式,那么我们实际上需要针对Root对象去进行OGNL表达式的计算并返回结果。

上下文环境(Context)

有了表达式和Root对象,我们已经可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。

不过,事实上,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context)将规定OGNL的操作在哪里干

OGNL的上下文环境是一个Map结构,称之为OgnlContext。之前我们所提到的Root对象(Root Object),事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。

后续

Q:以下Mapper.xml中,如参数传递

{
    "state": 0
}

时,是否会拼接SQL?

<if test="req.state != null and req.state != ''">
        AND md.state = #{req.state}
if>

state字段定义如下:

/**
 * 数据状态
 */
@Range(min = 0, max = 1, message = "状态只能为0(未处理),1(已处理)")
private Integer state;

A:不会拼接,因为在OGNL表达式中,0 == ‘’ 会返回true。

你可能感兴趣的:(数据库,mybatis)