手写Mybatis:第16章-解析含标签的动态SQL语句

news2024/11/29 3:55:04

文章目录

  • 一、目标:动态SQL语句
  • 二、设计:动态SQL语句
  • 三、实现:动态SQL语句
    • 3.0 依赖修改
    • 3.1 工程结构
    • 3.2 动态SQL语句类图
    • 3.3 基本类型注册器
    • 3.4 Ognl表达式处理
      • 3.4.1 Ognl类解析器
      • 3.4.2 Ognl缓存
      • 3.4.3 表达式求值器
    • 3.5 标签节点解析
      • 3.5.1 文本SQL节点
      • 3.5.2 拼装SQL节点
      • 3.5.3 判断SQL节点
    • 3.6 脚本构建器解析动态SQL
      • 3.6.1 动态SQL源码
      • 3.6.2 XML脚本构建器
    • 3.7 节点处理器及其实现
      • 3.7.1 节点处理器接口
      • 3.7.2 节点处理器接口实现类
      • 3.7.3 处理动态标签和SQL节点
  • 四、测试:动态SQL语句
    • 4.1 配置Mapper
    • 4.2 单元测试
    • 4.3 Ognl测试
  • 五、总结:动态SQL语句

一、目标:动态SQL语句

💡 解析含标签的动态SQL语句?

  • 之前我们在 Mapper XML 中配置的 SQL 一直都是静态的 SQL 语句,也就是说测试的是一条完整的 SQL 语句
    • 例如:select * from table where id = ?
  • 那么在实际的使用场景中,我们往往需要根据入参对象中的字段是否有值,判断后才被设置到 SQL 语句上。

在这里插入图片描述

  • 这样基于拼装 SQL 的方式,在 Mybatis 框架中是非常常用的。
  • 所以我们需要在现有对 Mapper XML 静态 SQL 的解析上,在 XML 脚本构建器中扩充对动态 SQL 的处理,最终让我们的 ORM 框架可以配置拼装 SQL 语句。

二、设计:动态SQL语句

💡 如何设计动态SQL语句?

  • 按照 XML 语句构建器,对 Mapper 文件的解析过程中。
    • 包括处理 insert/delete/update/select SQL 语句标签的参数类型、结果类型、命令类型等。
    • 以及创建 SqlSource 解析 SQL 信息。
    • 之前在对 SQL 信息的解析中,只做静态 SQL,不含有一些判断类型标签,如 trim 标签里的 if 操作等。
  • 扩展 SqlSource 的构建策略,提供另外一个实现类 DynamicSqlSource 用于解析动态 SQL
    • DynamicSqlSource 是一个非常常用的解析实现类。它的作用就是用于解析此类 SQL 文件。

在这里插入图片描述

  • 首先要在 XML 语句构建器中,扩展 XMLStatementBuilder#parseStatementNode 语句解析方法中 SqlSource 的处理。
    • SqlSource 中判断当前解析的 SQL 中是否有包含扩展的标签信息。
    • 这些标签来自于 Mybatis 定义的扩展标签类型,包括:trim/where/set/foreach/if/choose/when/otherwise/bing
  • 解析的过程分为循环拆解脚本节点,节点类型包括:TEXT_NODE、CDARTA_SECTION_NODE、ELEMENT_NODE
    • ELEMENT_NODE 就说明当前这个节点为自定义的 trim/if 等。
  • 循环拆解后读取到文本和标签内容。再把这个解析出来的 SqlNode 节点结合做简单的封装,并交给动态 SQL 操作类 DynamicSqlSource 进行处理,最终返回 SqlSource 语句。
  • 新增 SqlNode 节点处理的接口实现类,也都会完成 apply 接口方法的处理,完成表达式验证和填充 SQL 解析文本信息。

三、实现:动态SQL语句

3.0 依赖修改

pom.xml

<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
    <groupId>ognl</groupId>
    <artifactId>ognl</artifactId>
    <version>3.0.8</version>
    <scope>provided</scope>
    <optional>true</optional>
</dependency>

3.1 工程结构

mybatis-step-15
|-src
	|-main
	|	|-java
	|		|-com.lino.mybatis
    |			|-annotations
    |			|	|-Delete.java
	|			|	|-Insert.java
	|			|	|-Select.java
    |			|	|-Update.java
    |			|-binding
    |			|	|-MapperMethod.java
	|			|	|-MapperProxy.java
	|			|	|-MapperProxyFactory.java
    |			|	|-MapperRegistry.java
    |			|-builder
    |			|	|-annotations
    |			|	|	|-MapperAnnotationBuilder.java
    |			|	|-xml
    |			|	|	|-XMLConfigBuilder.java
    |			|	|	|-XMLMapperBuilder.java
    |			|	|	|-XMLStatementBuilder.java
    |			|	|-BaseBuilder.java
    |			|	|-MapperBuilderAssistant.java
    |			|	|-ParameterExpression.java
    |			|	|-ResultMapResolver.java
    |			|	|-SqlSourceBuilder.java
    |			|	|-StaticSqlSource.java
	|			|-datasource
	|			|	|-druid
	|			|	|	|-DruidDataSourceFacroty.java
	|			|	|-pooled
	|			|	|	|-PooledConnection.java
	|			|	|	|-PooledDataSource.java
	|			|	|	|-PooledDataSourceFacroty.java
	|			|	|	|-PoolState.java
	|			|	|-unpooled
	|			|	|	|-UnpooledDataSource.java
	|			|	|	|-UnpooledDataSourceFacroty.java
	|			|	|-DataSourceFactory.java
	|			|-executor
	|			|	|-keygen
	|			|	|	|-Jdbc3KeyGenerator.java
	|			|	|	|-KeyGenerator.java
	|			|	|	|-NoKeyGenerator.java
	|			|	|	|-SelectKeyGenerator.java
	|			|	|-parameter
	|			|	|	|-ParameterHandler.java
	|			|	|-result
	|			|	|	|-DefaultResultContext.java
	|			|	|	|-DefaultResultHandler.java
	|			|	|-resultset
	|			|	|	|-DefaultResultSetHandler.java
	|			|	|	|-ResultSetHandler.java
	|			|	|	|-ResultSetWrapper.java
	|			|	|-statement
	|			|	|	|-BaseStatementHandler.java
	|			|	|	|-PreparedStatementHandler.java
	|			|	|	|-SimpleStatementHandler.java
	|			|	|	|-StatementHandler.java
	|			|	|-BaseExecutor.java
	|			|	|-Executor.java
	|			|	|-SimpleExecutor.java
    |			|-io
    |			|	|-Resources.java
    |			|-mapping
    |			|	|-BoundSql.java
    |			|	|-Environment.java
    |			|	|-MappedStatement.java
    |			|	|-ParameterMapping.java
    |			|	|-ResultFlag.java
    |			|	|-ResultMap.java
    |			|	|-ResultMapping.java
    |			|	|-SqlCommandType.java
    |			|	|-SqlSource.java
    |			|-parsing
    |			|	|-GenericTokenParser.java
    |			|	|-TokenHandler.java
	|			|-reflection
	|			|	|-factory
	|			|	|	|-DefaultObjectFactory.java
	|			|	|	|-ObjectFactory.java
	|			|	|-invoker
	|			|	|	|-GetFieldInvoker.java
	|			|	|	|-Invoker.java
	|			|	|	|-MethodInvoker.java
	|			|	|	|-SetFieldInvoker.java
	|			|	|-property
	|			|	|	|-PropertyNamer.java
	|			|	|	|-PropertyTokenizer.java
	|			|	|-wrapper
	|			|	|	|-BaseWrapper.java
	|			|	|	|-BeanWrapper.java
	|			|	|	|-CollectionWrapper.java
	|			|	|	|-DefaultObjectWrapperFactory.java
	|			|	|	|-MapWrapper.java
	|			|	|	|-ObjectWrapper.java
	|			|	|	|-ObjectWrapperFactory.java
	|			|	|-MetaClass.java
	|			|	|-MetaObject.java
	|			|	|-Reflector.java
	|			|	|-SystemMetaObject.java
	|			|-scripting
	|			|	|-defaults
	|			|	|	|-DefaultParameterHandler.java
	|			|	|	|-RawSqlSource.java
	|			|	|-xmltags
	|			|	|	|-DynamicContext.java
	|			|	|	|-DynamicSqlSource.java
    |			|	|	|-ExpressionEvaluator.java
    |			|	|	|-IfSqlNode.java
	|			|	|	|-MixedSqlNode.java
    |			|	|	|-OgnlCache.java
    |			|	|	|-OgnlClassResolver.java
	|			|	|	|-SqlNode.java
    |			|	|	|-StaticTextSqlNode.java
	|			|	|	|-TextSqlNode.java
    |			|	|	|-TrimSqlNode.java
	|			|	|	|-XMLLanguageDriver.java
	|			|	|	|-XMLScriptBuilder.java
	|			|	|-LanguageDriver.java
	|			|	|-LanguageDriverRegistry.java
    |			|-session
    |			|	|-defaults
    |			|	|	|-DefaultSqlSession.java
    |			|	|	|-DefaultSqlSessionFactory.java
    |			|	|-Configuration.java
    |			|	|-ResultContext.java
    |			|	|-ResultHandler.java
    |			|	|-RowBounds.java
    |			|	|-SqlSession.java
    |			|	|-SqlSessionFactory.java
    |			|	|-SqlSessionFactoryBuilder.java
    |			|	|-TransactionIsolationLevel.java
    |			|-transaction
    |			|	|-jdbc
    |			|	|	|-JdbcTransaction.java
    |			|	|	|-JdbcTransactionFactory.java
    |			|	|-Transaction.java
    |			|	|-TransactionFactory.java
    |			|-type
    |			|	|-BaseTypeHandler.java
    |			|	|-DateTypeHandler.java
    |			|	|-IntegerTypeHandler.java
    |			|	|-JdbcType.java
    |			|	|-LongTypeHandler.java
    |			|	|-SimpleTypeRegistry.java
    |			|	|-StringTypeHandler.java
    |			|	|-TypeAliasRegistry.java
    |			|	|-TypeHandler.java
    |			|	|-TypeHandlerRegistry.java
	|-test
		|-java
		|	|-com.lino.mybatis.test
		|	|-dao
		|	|	|-IActivityDao.java
		|	|-po
		|	|	|-Activity.java
		|	|-ApiTest.java
        |-resources
			|-mapper
			|	|-Activity_Mapper.xml
        	|-mybatis-config-datasource.xml

3.2 动态SQL语句类图

在这里插入图片描述

  • 通过 XML 语句驱动器 XMLLanguageDriver,创建的 XML 脚本构建器解析 XMLScriptBuilder 为入口,开始执行脚本的解析操作。
  • 在整个解析的过程中,扩展了 SqlSource 的动态 SQL 解析 DynamicSqlSource、新增了 SqlNode 节点处理,以及对应的 NodeHandler 节点处理器。
  • 而关于整个 ORM 框架的执行流程,从解析、存放、执行、返回等。
    • 其实处理扩展了这部分对动态 SQL 的处理,其余的流程不需要做任何扩展。
    • 这里体现了框架结构关于解耦设计的重要性。

3.3 基本类型注册器

SimpleTypeRegistry.java

package com.lino.mybatis.type;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * @description: 基本类型注册器
 */
public class SimpleTypeRegistry {

    private static final Set<Class<?>> SIMPLE_TYPE_SET = new HashSet<>();

    static {
        SIMPLE_TYPE_SET.add(String.class);
        SIMPLE_TYPE_SET.add(Byte.class);
        SIMPLE_TYPE_SET.add(Short.class);
        SIMPLE_TYPE_SET.add(Character.class);
        SIMPLE_TYPE_SET.add(Integer.class);
        SIMPLE_TYPE_SET.add(Long.class);
        SIMPLE_TYPE_SET.add(Float.class);
        SIMPLE_TYPE_SET.add(Double.class);
        SIMPLE_TYPE_SET.add(Boolean.class);
        SIMPLE_TYPE_SET.add(Date.class);
        SIMPLE_TYPE_SET.add(Class.class);
        SIMPLE_TYPE_SET.add(BigInteger.class);
        SIMPLE_TYPE_SET.add(BigDecimal.class);
    }

    public SimpleTypeRegistry() {
    }

    /**
     * 是否是基本类型
     *
     * @param clazz 类型
     * @return 是否
     */
    public static boolean isSimpleType(Class<?> clazz) {
        return SIMPLE_TYPE_SET.contains(clazz);
    }
}

3.4 Ognl表达式处理

3.4.1 Ognl类解析器

OgnlClassResolver.java

package com.lino.mybatis.scripting.xmltags;

import com.lino.mybatis.io.Resources;
import ognl.ClassResolver;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: Ognl类解析器
 */
public class OgnlClassResolver implements ClassResolver {

    private Map<String, Class<?>> classes = new HashMap<>(101);

    @Override
    public Class classForName(String className, Map context) throws ClassNotFoundException {
        Class<?> result = null;
        if ((result = classes.get(className)) == null) {
            try {
                result = Resources.classForName(className);
            } catch (ClassNotFoundException e1) {
                if (className.indexOf('.') == -1) {
                    result = Resources.classForName("java.lang" + className);
                    classes.put("java.lang." + className, result);
                }
            }
            classes.put(className, result);
        }
        return result;
    }
}

3.4.2 Ognl缓存

OgnlCache.java

package com.lino.mybatis.scripting.xmltags;

import ognl.Ognl;
import ognl.OgnlException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description: OGNL缓存 http://code.google.com/p/mybatis/issues/detail?id=342
 * OGNL 是 Object-Graph Navigation Language 的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL)
 * 通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。
 * 它使用相同的表达式去存取对象的属性。
 */
public class OgnlCache {

    private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();

    public OgnlCache() {
    }

    public static Object getValue(String expression, Object root) {
        try {
            Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
            return Ognl.getValue(parseExpression(expression), context, root);
        } catch (OgnlException e) {
            throw new RuntimeException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
        }
    }

    private static Object parseExpression(String expression) throws OgnlException {
        Object node = expressionCache.get(expression);
        if (node == null) {
            // OgnlParser.topLevelExpression 操作耗时,加个缓存放到 ConcurrentHashMap 里面
            node = Ognl.parseExpression(expression);
            expressionCache.put(expression, node);
        }
        return node;
    }

}

3.4.3 表达式求值器

ExpressionEvaluator.java

package com.lino.mybatis.scripting.xmltags;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @description: 表达式求值器
 */
public class ExpressionEvaluator {

    /**
     * 表达式求布尔值,比如 username == 'xiaoling'
     *
     * @param expression      表达式
     * @param parameterObject 参数对象
     * @return 布尔值
     */
    public boolean evaluateBoolean(String expression, Object parameterObject) {
        // 非常简单,就是调用ognl
        Object value = OgnlCache.getValue(expression, parameterObject);
        if (value instanceof Boolean) {
            // 如果是Boolean
            return (Boolean) value;
        }
        if (value instanceof Number) {
            // 如果是Number,判断不为0
            return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
        }
        // 否则判断不为null
        return value != null;
    }

    public Iterable<?> evaluateIterator(String expression, Object parameterObject) {
        // 原生的ognl很强大,OgnlCache.getValue直接就可以返回一个Iterable型或数组型或Map型
        Object value = OgnlCache.getValue(expression, parameterObject);
        if (value == null) {
            throw new RuntimeException("The expression '" + expression + "' evaluated to a null value.");
        }
        if (value instanceof Iterable) {
            return (Iterable<?>) value;
        }
        if (value.getClass().isArray()) {
            // 如果是array,则把他变成一个List<Object>
            // 注释下面提到了,不能用Arrays.asList(),因为array可能是基本型,这样会出ClassCastException,
            // 见https://code.google.com/p/mybatis/issues/detail?id=209
            // the array may be primitive, so Arrays.asList() may throw
            // a ClassCastException (issue 209).  Do the work manually
            // Curse primitives! :) (JGB)
            int size = Array.getLength(value);
            List<Object> answer = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                Object o = Array.get(value, i);
                answer.add(o);
            }
            return answer;
        }
        if (value instanceof Map) {
            return ((Map) value).entrySet();
        }
        throw new RuntimeException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
    }
}
  • 根据 expression 获取对应的结果值,其实核心调用的是 Ognl.parseExpression(expression)
    • 不过此方法有些耗时,因此被加入到缓存提供服务。
  • 这个方法会根据你的表达式和入参信息,判断是否满足条件。
    • 例如:null != activityId 那么就会判断 parameterObject 的入参是否包含了 activityId 对应的属性值。

3.5 标签节点解析

  • 因为有了动态 SQL 的处理,相当于在 SQL 语句中包含:test文本、trim拼装、if判断,所以为了解决这一差异化的解析,需要提供不同的处理策略。
    • 为此需要实现已有的 StaticTextSqlNode 静态文本 SQL 节点所实现的接口。
    • 新增 TextSqlNodeTrimSqlNodeIfSqlNode 这些实现类。

3.5.1 文本SQL节点

TextSqlNode.java

package com.lino.mybatis.scripting.xmltags;

import com.lino.mybatis.parsing.GenericTokenParser;
import com.lino.mybatis.parsing.TokenHandler;
import com.lino.mybatis.type.SimpleTypeRegistry;
import java.util.regex.Pattern;

/**
 * @description: 文本SQL节点(CDATA | TEXT)
 */
public class TextSqlNode implements SqlNode {

    private String text;
    private Pattern injectionFilter;

    public TextSqlNode(String text) {
        this(text, null);
    }

    public TextSqlNode(String text, Pattern injectionFilter) {
        this.text = text;
        this.injectionFilter = injectionFilter;
    }

    /**
     * 判断是否是动态sql
     */
    public boolean isDynamic() {
        DynamicCheckTokenParser checker = new DynamicCheckTokenParser();
        GenericTokenParser parser = createParser(checker);
        parser.parse(text);
        return checker.isDynamic();
    }

    @Override
    public boolean apply(DynamicContext context) {
        GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
        context.appendSql(parser.parse(text));
        return true;
    }

    private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
    }

    /**
     * 绑定记号解析器
     */
    private static class BindingTokenParser implements TokenHandler {

        private DynamicContext context;
        private Pattern injectionFilter;

        public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
            this.context = context;
            this.injectionFilter = injectionFilter;
        }

        @Override
        public String handleToken(String content) {
            Object parameter = context.getBindings().get("_parameter");
            if (parameter == null) {
                context.getBindings().put("value", null);
            } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                context.getBindings().put("value", parameter);
            }
            // 从缓存里取得值
            Object value = OgnlCache.getValue(content, context.getBindings());
            String srtValue = (value == null ? "" : String.valueOf(value));
            checkInjection(srtValue);
            return srtValue;
        }

        /**
         * 检查是否匹配正则表达式
         *
         * @param value 值
         */
        private void checkInjection(String value) {
            if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
                throw new RuntimeException("Invalid input. Please conform to regex" + injectionFilter.pattern());
            }
        }
    }

    /**
     * 动态SQL检查器
     */
    private static class DynamicCheckTokenParser implements TokenHandler {

        private boolean isDynamic;

        public DynamicCheckTokenParser() {
        }

        public boolean isDynamic() {
            return isDynamic;
        }

        @Override
        public String handleToken(String content) {
            // 设置 isDynamic 为 true,即调用了这个类就必定是动态SQL
            this.isDynamic = true;
            return null;
        }
    }
}
  • TextSqlNode#apply 方法用于解析 CDATA | TEXTSQL 节点,使用 GenericTokenParser 记号解析器进行处理操作。
  • 这部分用到 OGNL 表达式,这个工具组件可以帮助我们非常方便的获取对象的属性值信息。
    • 例如:Ognl.getValue("属性名称", context, root)

3.5.2 拼装SQL节点

TrimSqlNode.java

package com.lino.mybatis.scripting.xmltags;

import com.lino.mybatis.session.Configuration;
import java.util.*;

/**
 * @description: trim Sql Node 节点解析
 */
public class TrimSqlNode implements SqlNode {

    private SqlNode contents;
    private String prefix;
    private String suffix;
    private List<String> prefixesToOverride;
    private List<String> suffixesToOverride;
    private 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;
    }

    private static List<String> parseOverrides(String overrides) {
        if (overrides != null) {
            final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
            final List<String> list = new ArrayList<>(parser.countTokens());
            while (parser.hasMoreTokens()) {
                list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
            }
            return list;
        }
        return Collections.emptyList();
    }

    private class FilteredDynamicContext extends DynamicContext {

        private DynamicContext delegate;
        private boolean prefixApplied;
        private boolean suffixApplied;
        private StringBuilder sqlBuffer;

        public FilteredDynamicContext(DynamicContext delegate) {
            super(configuration, null);
            this.delegate = delegate;
            this.prefixApplied = false;
            this.suffixApplied = false;
            this.sqlBuffer = new StringBuilder();
        }

        public void applyAll() {
            sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
            String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
            if (trimmedUppercaseSql.length() > 0) {
                applyPrefix(sqlBuffer, trimmedUppercaseSql);
                applySuffix(sqlBuffer, trimmedUppercaseSql);
            }
            delegate.appendSql(sqlBuffer.toString());
        }

        @Override
        public Map<String, Object> getBindings() {
            return delegate.getBindings();
        }

        @Override
        public void bind(String name, Object value) {
            delegate.bind(name, value);
        }

        @Override
        public int getUniqueNumber() {
            return delegate.getUniqueNumber();
        }

        @Override
        public void appendSql(String sql) {
            sqlBuffer.append(sql);
        }

        @Override
        public String getSql() {
            return delegate.getSql();
        }

        private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!prefixApplied) {
                prefixApplied = true;
                if (prefixesToOverride != null) {
                    for (String toRemove : prefixesToOverride) {
                        if (trimmedUppercaseSql.startsWith(toRemove)) {
                            sql.delete(0, toRemove.trim().length());
                            break;
                        }
                    }
                }
                if (prefix != null) {
                    sql.insert(0, " ");
                    sql.insert(0, prefix);
                }
            }
        }

        private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!suffixApplied) {
                suffixApplied = true;
                if (suffixesToOverride != null) {
                    for (String toRemove : suffixesToOverride) {
                        if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
                            int start = sql.length() - toRemove.trim().length();
                            int end = sql.length();
                            sql.delete(start, end);
                            break;
                        }
                    }
                }
                if (suffix != null) {
                    sql.append(" ");
                    sql.append(suffix);
                }
            }
        }
    }
}
  • TrimSqlNode 节点的解析,主要依赖于 FilteredDynamicContext 对配置信息的拼装,把 AND | OR 等,拼装到 SQL 语句上进行返回处理。

3.5.3 判断SQL节点

IfSqlNode.java

package com.lino.mybatis.scripting.xmltags;

/**
 * @description: IF SQL 节点
 */
public class IfSqlNode implements SqlNode {

    private ExpressionEvaluator evaluator;
    private String test;
    private SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
        this.test = test;
        this.contents = contents;
        this.evaluator = new ExpressionEvaluator();
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 如果满足条件,则apply,并返回true
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            contents.apply(context);
            return true;
        }
        return false;
    }
}
  • IfSqlNode 的标签处理,主要是验证标签表达式是否满足要求。而这个判断操作就是使用 OGNL 表达式进行处理。

3.6 脚本构建器解析动态SQL

  • 所有的 XMLSQL 解析操作都会进入到 XMLScriptBuider 脚本构建器中进行处理。
  • 扩展:循环检测 insert/delete/update/select 标签传递进来的 SQL,基于传递进来的 SQL 语句检测是否包含了额外的标签。
    • 标签trim/where/set/foreach/if/choose/when/otherwise/bing

3.6.1 动态SQL源码

DynamicSqlSource.java

package com.lino.mybatis.scripting.xmltags;

import com.lino.mybatis.builder.SqlSourceBuilder;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.session.Configuration;
import java.util.Map;

/**
 * @description: 动态SQL源码
 */
public class DynamicSqlSource implements SqlSource {

    private Configuration configuration;
    private SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 生成一个 DynamicContext 动态上下文
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // SqlNode.apply 将 ${} 参数替换掉,不替换 #{} 这种参数
        rootSqlNode.apply(context);

        // 调用 SqlSourceBuilder
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();

        // SqlSourceBuilder.parse 这里返回的是 StaticSqlSource,解析过程就把那些参数都替换成?了,也就是最基本的JDBC的SQL语句
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

        // SqlSource.getBoundSql,非递归调用,而是调用 StaticSqlSource 实现类
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
    }
}
  • DynamicSqlSource#getBoundSql 的接口方法中,包括:生成解析元素的动态上下文、替换占位符、通过 SqlSourceBuilder SQL 源码构建器,提供的 parse 解析方法,返回静态 SQL 语句。
    • 其实最后异步就相当于把即使是动态 SQL 也归为静态的 SQL 语句来处理。
  • 最后基于返回的 SqlSource 以及参数信息,构建出 BoundSql 把相关的参数类型、返回类型等信息填充。
  • 另外这里 rootSqlNode.apply(context),对 trim、if 等标签的内容会调用到具体的 SqlNode 节点实现类中。

3.6.2 XML脚本构建器

XMLScriptBuilder.java

package com.lino.mybatis.scripting.xmltags;

import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.defaults.RawSqlSource;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;
import org.dom4j.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @description: XML脚本构建器
 */
public class XMLScriptBuilder extends BaseBuilder {

    private Element element;
    private boolean isDynamic;
    private Class<?> parameterType;
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    public XMLScriptBuilder(Configuration configuration, Element element, Class<?> parameterType) {
        super(configuration);
        this.element = element;
        this.parameterType = parameterType;
        initNodeHandlerMap();
    }

    private void initNodeHandlerMap() {
        // 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bind
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("if", new IfHandler());
    }

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

    private List<SqlNode> parseDynamicTags(Element element) {
        List<SqlNode> contents = new ArrayList<>();
        List<Node> children = element.content();
        for (Node child : children) {
            if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {
                String data = child.getText();
                TextSqlNode textSqlNode = new TextSqlNode(data);
                if (textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(data));
                }
            } else if (child.getNodeType() == Node.ELEMENT_NODE) {
                String nodeName = child.getName();
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {
                    throw new RuntimeException("Unknown element <" + nodeName + "> in SQL statement.");
                }
                handler.handleNode(element.element(child.getName()), contents);
                isDynamic = true;
            }
        }
        return contents;
    }

    private interface NodeHandler {
        void handleNode(Element nodeToHandle, List<SqlNode> targetContents);
    }

    private class TrimHandler implements NodeHandler {
        @Override
        public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
            List<SqlNode> contents = parseDynamicTags(nodeToHandle);
            MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
            String prefix = nodeToHandle.attributeValue("prefix");
            String prefixOverrides = nodeToHandle.attributeValue("prefixOverrides");
            String suffix = nodeToHandle.attributeValue("suffix");
            String suffixOverrides = nodeToHandle.attributeValue("suffixOverrides");
            TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
            targetContents.add(trim);
        }
    }

    private class IfHandler implements NodeHandler {
        @Override
        public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
            List<SqlNode> contents = parseDynamicTags(nodeToHandle);
            MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
            String test = nodeToHandle.attributeValue("test");
            IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
            targetContents.add(ifSqlNode);
        }
    }
}

3.7 节点处理器及其实现

3.7.1 节点处理器接口

NodeHandler.java

  • 定义节点处理器接口,所有的标签处理也都是基于这个接口来实现的
private interface NodeHandler {
    void handleNode(Element nodeToHandle, List<SqlNode> targetContents);
}

3.7.2 节点处理器接口实现类

TrimHandler.java

private class TrimHandler implements NodeHandler {
    @Override
    public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
        List<SqlNode> contents = parseDynamicTags(nodeToHandle);
        MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
        String prefix = nodeToHandle.attributeValue("prefix");
        String prefixOverrides = nodeToHandle.attributeValue("prefixOverrides");
        String suffix = nodeToHandle.attributeValue("suffix");
        String suffixOverrides = nodeToHandle.attributeValue("suffixOverrides");
        TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
        targetContents.add(trim);
    }
}

private class IfHandler implements NodeHandler {
    @Override
    public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
        List<SqlNode> contents = parseDynamicTags(nodeToHandle);
        MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
        String test = nodeToHandle.attributeValue("test");
        IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
        targetContents.add(ifSqlNode);
    }
}
  • <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">...</trim>
  • 解析 trim 标签信息,把字段 prefix、prefixOverrides、suffixOverrides 都依次获取出来,使用 TrimSqlNode 构建后存放到 List<SqlNode> 中。
  • <if test="null != activityId">...</if>
  • 解析 if 语句标签,与解析 trim 标签类似,获取标签配置 test 语句表达式,使用 IfSqlNode 进行构建,构建后存放到 List<SqlNode> 中。

3.7.3 处理动态标签和SQL节点

private void initNodeHandlerMap() {
    // 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bind
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("if", new IfHandler());
}

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

private List<SqlNode> parseDynamicTags(Element element) {
    List<SqlNode> contents = new ArrayList<>();
    List<Node> children = element.content();
    for (Node child : children) {
        if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) {
            String data = child.getText();
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNodeType() == Node.ELEMENT_NODE) {
            String nodeName = child.getName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new RuntimeException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(element.element(child.getName()), contents);
            isDynamic = true;
        }
    }
    return contents;
}
  • initNodeHandlerMap 中初始化 trim、if 标签处理。
  • parseDynamicTags 中处理解析动态 SQL、静态 SQL 前,需要对 parseDynamicTags 进行扩展。
    • 因为这里已经在 SQL 标签里配置了拼装 SQL 类标签,所以解析的过程会变得复杂一些。
    • 在解析过程中,先获取当前 Element 元素下的 Node 节点结合,再根据这些节点的类型,TEXT_NODE、CDATA_SECTION_NODE、ELEMENT_NODE 进行不同的解析处理。
    • 当循环处理中遇到了新的 Element 元素,则从 nodeHandlerMap 查找到对应的标签处理器进行处理。
    • 注意Mybatis 源码采用的是 w3c dom 解析处理。
  • parseScriptNode 中关于动态 SQL 和 静态 SQL 的解析处理,根据 isDynamic 判断选择不同的解析方式,并最终返回 SqlSource 信息。

四、测试:动态SQL语句

4.1 配置Mapper

Activity_Mapper.xml

<?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.lino.mybatis.test.dao.IActivityDao">

    <resultMap id="activityMap" type="com.lino.mybatis.test.po.Activity">
        <id column="id" property="id"/>
        <result column="activity_id" property="activityId"/>
        <result column="activity_name" property="activityName"/>
        <result column="activity_desc" property="activityDesc"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <select id="queryActivityById" parameterType="com.lino.mybatis.test.po.Activity" resultMap="activityMap">
        SELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
        <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
            <if test="null != activityId">
                activity_id = #{activityId}
            </if>
        </trim>
    </select>
</mapper>
  • 在查询语句的配置中,新增加了 trim、if 用于拼接 SQL 标签。

4.2 单元测试

ApiTest.java

@Test
public void test_queryActivityById() {
    // 1.获取映射器对象
    IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
    // 2.测试验证
    Activity activity = new Activity();
    activity.setActivityId(100001L);
    Activity result = dao.queryActivityById(activity);
    logger.info("测试结果:{}", JSON.toJSONString(result));
}

测试结果

10:55:40.554 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1202683709.
10:55:40.569 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
10:55:40.569 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}

在这里插入图片描述

  • 根据在 XMLScriptBuilder 脚本构建器中添加断点调试,以及输出结果,可以验证实现的标签拼装 SQL 语句,可以正常运行。

4.3 Ognl测试

ApiTest.java

@Test
public void test_ognl() throws OgnlException {
    Activity activity = new Activity();
    activity.setActivityId(1L);
    activity.setActivityName("测试活动");
    activity.setActivityDesc("测试内容");

    OgnlContext context = new OgnlContext();
    context.setRoot(activity);
    Object root = context.getRoot();

    Object activityName = Ognl.getValue("activityName", context, root);
    Object activityDesc = Ognl.getValue("activityDesc", context, root);
    Object value = Ognl.getValue("activityDesc.length()", context, root);

    System.out.println(activityName + "\t" + activityDesc + " length:" + value);
}

测试结果

测试活动	测试内容 length:4

五、总结:动态SQL语句

  • 扩充解析策略。首先是对静态SQL的解析扩展到对动态SQL解析的操作,之后对于动态SQL中含有的标签,提供不同的解析策略。
    • trim、if,对于一些其他的标签 where、set、foreach 等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/975604.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智能电力运维系统

力安科技智能电力运维系统依托电易云-智慧电力物联网&#xff0c;利用“互联网”的思维模式&#xff0c;通过计算机网络、大数据、云计算、物联网等技术打造“智能电力运维云平台”&#xff0c;采用“线上监管线下维护”深度融合的方式&#xff0c;创新实现全方位主动运维&…

SpringBoot整合MQ

1.创建工程并引入依赖 <!-- 添加rocketmq的启动器--><dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.1.1</version></dependency>2.编写…

toFixed() 保留小数

let num item.value / total;item.rate parseFloat(num).toFixed(4) * 100 "%"; 不用parseFloat 有时会失真

使用java代码给Excel加水印,代码全,进阶版

以下代码&#xff0c;亲测可以跑通 1、上一篇博客用了Apache POI库3.8的版本的形式对Excel加了水印&#xff0c;但是最近主线版本用了4.1.2的形式&#xff0c;由于为了保持版本的兼容性&#xff0c;下面有开发了Apache POI的4.1.2的版本号的方案。 pom文件为&#xff1a; <d…

如何培养潜在客户?看完这篇你就懂了

图片来源于&#xff1a;SaleSmartly官网 有效的潜在客户培育策略将帮助您将更多潜在客户转化为付费客户。 但是&#xff0c;这并不总是那么容易——您必须与其他公司争夺受众的注意力&#xff0c;并向您的领导证明为什么值得投资您的产品或服务。在本文中&#xff0c;我将向您展…

手写call方法

<script>/*手写call方法1.定义myCall方法2.设置this并调用函数3.接收剩余参数 */Function.prototype.myCall function myCall (thisArg, ...args) {// 1.设置this并调用函数//给thisArg添加一个一定和原属性不重名的新属性&#xff08;方法&#xff09;//使用symbol来保…

TikTok Shop启动东南亚跨境9.9大促,重要性类比“黑五”

TikTok Shop启动东南亚跨境9.9大促 据了解&#xff0c;TikTok Shop即将开启东南亚99大促活动&#xff0c;其重要程度可类比于“中国的双11”“美国的黑色星期五”等购物节日&#xff0c;且整合了包括马来西亚、新加坡、菲律宾、越南和泰国五个国家站点的大促资源、推出相关的流…

VMware 虚拟机安装

目录 ​编辑 一、环境说明 1.1 VMware 版本 1.2 系统镜像版本 二、VMware环境安装 2.1 下载VMware 2.2 VMware安装 三、安装CentOS-8.3.2011虚拟机系统 3.1 新建VMware虚拟机 3.2 安装程序光盘映像文件&#xff08;iso&#xff09; 3.3 设置账号密码 3.4 设置虚拟机…

【会议征稿】第五届土木工程、环境资源与能源材料国际学术会议(CCESEM 2023)

第五届土木工程、环境资源与能源材料国际学术会议&#xff08;CCESEM 2023&#xff09; 第五届土木工程、环境资源与能源材料国际学术会议&#xff08;CCESEM 2023&#xff09;&#xff0c;定于2023年10月27日至29日在厦门举行。会议主要围绕“土木工程”、“环境资源”、“能…

Automotive 添加一个特权APP

Automotive 添加一个特权APP platform: android-13.0.0_r32 一. 添加一个自定义空调的app为例 路径&#xff1a;packages/apps/Car/MyHvac app内容可以自己定义&#xff0c;目录结构如下&#xff1a; 1.1 Android.bp package {default_applicable_licenses: ["Andr…

软件生命周期及流程

软件生命周期&#xff1a; 软件生命周期(SDLC&#xff0c;Systems Development Life Cycle)是软件开始研制到最终被废弃不用所经历的各个阶段. 需求分析阶段--输出需求规格说明书&#xff08;原型图&#xff09; 测试介入的晚--回溯成本高 敏捷开发模型&#xff1a; 从1990年…

一文读懂|内核顺序锁

Linux 内核有非常多的锁机制&#xff0c;如&#xff1a;自旋锁、读写锁、信号量和 RCU 锁等。本文介绍一种和读写锁比较相似的锁机制&#xff1a;顺序锁&#xff08;seqlock&#xff09;。 顺序锁与读写锁一样&#xff0c;都是针对多读少写且快速处理的锁机制。而顺序锁和读写…

【SQL学习笔记】关系模型与查询和更新数据

一、关系模型 1.1 主键 主键是关系表中记录的唯一标识。主键的选取非常重要&#xff1a;主键不要带有业务含义&#xff0c;而应该使用BIGINT自增或者GUID类型。主键也不应该允许NULL。 可以使用多个列作为联合主键&#xff0c;但联合主键并不常用。 1.2 外键 FOREIGN KEY …

WEB APIs day6

一、正则表达式 RegExp是正则表达式的意思 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" co…

异步驱动电机总成汇总

特斯拉双电机 蔚来ET7异步电驱 蔚来ET5异步电驱 问界M5异步电驱 比亚迪海豹异步异步电驱 汇川800v异步电驱 阿维塔异步电驱 小鹏G6异步电驱 小鹏G9异步电驱 大众ID4异步电驱 奥迪etron异步电驱 欢迎补充&#xff5e;&#xff5e;&#xff5e;欢迎转载&#xff01;&#xff01;&…

适合心理法律在线咨询预约含视频图文电话咨询功能的小程序开发

目前智能手机普及&#xff0c;很多以前需要线下咨询的场景都被搬到了线上&#xff0c;这样既可以使咨询者更方便&#xff0c;也可以使被咨询者接待效率更高&#xff0c;服务更多咨询者。基于此我们开发了专门的一款具有线上咨询功能的小程序&#xff0c;同时为了方便被咨询者服…

算法笔记:点四叉树

点四叉树是一种用于主要是针对空间点存储与索引的树形数据结构在点四叉树中&#xff0c;空间被分割成四个矩形&#xff0c;四个不同的多边形对应于SW、NW、SE、NE四个象限 1 基本操作 1.1 初始化 创建一个根节点&#xff0c;该节点代表整个二维空间区域 1.2 插入点 当一个新…

深入理解 JVM 之——垃圾回收与内存分配策略

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 垃圾回收策略 说起垃圾收集&#xff08;Garbage Collection&#xff0c;下文简称GC&#xff09;&#xff0c;有不少人把这项技术当作Java语言的伴生产物。事实上&#xff0c;垃圾收集的历史远远比Java久远&…

【力扣周赛】第 360 场周赛(贪心 ⭐树上倍增)

文章目录 竞赛链接Q1&#xff1a;8015. 距离原点最远的点&#xff08;贪心&#xff09;Q2&#xff1a;8022. 找出美丽数组的最小和&#xff08;贪心&#xff09;Q3&#xff1a;2835. 使子序列的和等于目标的最少操作次数&#xff08;贪心&#xff09;思路竞赛时丑陋代码&#x…

SquirrelMail实现Web方式收发邮件_xionglling的博客-CSDN博客

SquirrelMail实现Web方式收发邮件_xionglling的博客-CSDN博客小松鼠实现Web邮件服务SquirrelMail 是一个用PHP开发的Web邮件系统。它内置纯PHP支持的IMAP和SMTP协议&#xff0c;所有页面都遵循 HTML 4.0标准(没有使用任何 JavaScript 代码)&#xff0c;以便最大限度兼容各种多浏…