手敲MyBatis(十四章)-解析含标签的动态SQL语句

发布时间:2024年01月03日

1.前言

这一章主要的就是要解析动态标签里的Sql语句,然后进行条件语句的拼接,动态标签实现了trim和if标签,所以Sql节点就要加上TrimSqlNode和ifSqlNode,我们最终要获取Sql源,动态Sql语句需要一些处理,所以需要添加DynamicSqlSource来处理动态Sql语句的调用和一些业务逻辑处理。

本章节主要就是要处理如下图片的解析Sql内容,把如下图片的Sql内容更改为能够可执行的Sql语句,这个是目标。

需要注意的是,sql语句不加条件时我们叫静态SQL,当动态语句标签包含的条件语句时,除了trim和if放入到对应的节点里,if里的Sql也要放入静态节点里,最后把这些个节点集合放入到混合节点里,等使用时直接遍历混合节点数据即可,最终调度到不同的节点取出Sql进行拼接即可。

2.xml类图

动态标签我们看作是一个节点,那么我们解析的xml的Sql语句里边就有很多的不同的节点,静态的Sql节点,if节点,trim节点,那么需要把不同节点的信息存储到不同的节点里,这块的功能需要在xml脚本构建类里实现,从这里开始去一点一点构建不同的节点。

节点构建完毕需要获取不同的文本进行SQL语句拼接,拼接完毕放入到DynamicContext的sqlBuilder里, DynamicSqlSource就可以根据DynamicContext直接获取到SQL了。

3.代码

因为我们要处理的是Sql里的内容,所以在代码设计里就是要处理Xml的脚本构建,也就是XMLScriptBuilder类,我们在XMLScriptBuilder类里添加了NodeHandler接口,定义了handleNode方法。

3.1 节点处理器(NodeHandler)

包名路径:package cn.bugstack.mybatis.scripting.xmltags;

然后定义两个实现类,TrimHandler和IfHandler类,TrimHandler主要解析trim标签,IfHandler主要处理if标签内容。

最后再初始化nodeHandler把TrimHandler和IfHandler放入到Map里,留着后面从Map取出使用。

public class XMLScriptBuilder extends BaseBuilder {
   // 过滤其他

    public XMLScriptBuilder(Configuration configuration, Element element, Class<?> parameterType) {
        super(configuration);
        this.element = element;
        this.parameterType = parameterType;
        initNodeHandlerMap();
    }
    
   // step-15新增
    private void initNodeHandlerMap() {
        // 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bind
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("if", new IfHandler());
    }
   

   // 节点处理器
   private interface NodeHandler {
       void handleNode(Element nodeToHandle, List<SqlNode> targetContents);
   }


    /**
     * <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">...</trim> 解析 trim 标签信息,把字段 prefix、
     * prefixOverrides、suffixOverrides 都依次获取出来,使用 TrimSqlNode 构建后存放到 List<SqlNode>   中。
     * 得到trim的属性
     */
    // step-15新增
    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);
        }
    }

    /**
     * <if test="null != activityId">...</if> 解析if语句标签,与解析 trim 标签类似,
     * 获取标签配置 test 语句表达式,使用 IfSqlNode 进行构建,构建后存放到 List<SqlNode>  中。
     * 得到if标签的属性
     */
    // step-15新增
    private class IfHandler implements NodeHandler {
        @Override
        public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
            List<SqlNode> contents = parseDynamicTags(nodeToHandle);
            MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
            // 得到test的判断语句
            String test = nodeToHandle.attributeValue("test");
            IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
            targetContents.add(ifSqlNode);
        }
    }


}

3.2?SqlNode

我们原来的SqlNode有静态的和混合的实现类,这次我们还要加三个实现类,TextSqlNode和IfSqlNode以及TrimSqlNode。

3.2.1?TextSqlNode

TextSqlNode:此节点处理是否是动态Sql的判断,还有一个是${}的参数替换。

/**
 * @Author df
 * @Description: 文本SQL节点(CDATA | TEXT)
 * @Date 2023/12/22 14:09
 */
// step-15新增
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() {
        DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
        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();
            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;
        }

        // 检查是否匹配正则表达式
        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 DynamicCheckerTokenParser implements TokenHandler {

        private boolean isDynamic;

        public DynamicCheckerTokenParser() {
            // Prevent Synthetic Access
        }

        public boolean isDynamic() {
            return isDynamic;
        }

        @Override
        public String handleToken(String content) {
            // 设置 isDynamic 为 true,即调用了这个类就必定是动态 SQL
            this.isDynamic = true;
            return null;
        }
    }
}

3.2.2?IfSqlNode

IfSqlNode:它专门就是处理test的内容判断的,如果满足判断则进入拼接Sql语句

/**
 * @Author df
 * @Description: IF SQL 节点
 * @Date 2023/12/22 15:17
 */
// step-15新增
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();
    }

    /**
     * <if test="null != activityId">
     * activity_id = #{activityId}
     * </if>
     */
    @Override
    public boolean apply(DynamicContext context) {
        // 如果满足条件,则apply,并返回true
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            // 拼接if标签里的Sql语句
            contents.apply(context);
            return true;
        }
        return false;
    }
}

ExpressionEvaluator类:处理if的test内容判断的

public class ExpressionEvaluator {
    // 表达式求布尔值,比如 username == 'xiaofuge'
    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;
    }
}

?OgnlCache:OGNL缓存,处理if的表达式判断,并把表达式存储起来。

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

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

    private OgnlCache() {
        // Prevent Instantiation of Static Class
    }

    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) {
            node = Ognl.parseExpression(expression);
            expressionCache.put(expression, node);
        }
        return node;
    }
}

OgnlClassResolver:?自己实现个OgnlClassResolver类加载器

public class OgnlClassResolver implements ClassResolver {

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

    @Override
    public Class classForName(String className, Map map) 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.2.3?TrimSqlNode

TrimSqlNode:trim最主要的就是调用处理if的sql节点处理或拼接,然后得到trim属性,把trim属性前缀或后缀进行拼接Sql处理。

这里的trim的Sql拼接处理都在其内部类实现,是FilteredDynamicContext类。

/**
 * @Author df
 * @Description: trim Sql Node 节点解析
 * @Date 2023/12/22 14:57
 */
// step-15新增
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);
        // 得到trim里的内容,进行处理,最后拼接语句
        // 例如:trim->if->条件语句
        boolean result = contents.apply(filteredDynamicContext);
        // 根据trim的属性添加前后缀
        filteredDynamicContext.applyAll();
        return result;
    }

    /**
     * <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
     *
     * </trim>
     * 将prefixOverrides以list形式展示
     */
    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().toLowerCase(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);
            }
            // 添加完拼接的前后缀,继续拼接完整的Sql
            delegate.appendSql(sqlBuffer.toString());
        }

        // 获取当前属性
        @Override
        public Map<String, Object> getBindings() {
            return delegate.getBindings();
        }

        // 拼接当前Sql
        @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);
                }
            }
        }


    }

}

3.3 DynamicSqlSource

在处理完Sql语句需要包装成SqlSource源,之前处理的都是静态源,这次我们需要加动态源类DynamicSqlSource类,实现SqlSource。

1.这个动态源主要是将所有的SqlNode进行处理,拼接为一个处理过的Sql然后直接返回Sql。

2.最后返回SqlSource时判断是否是动态的,是返回DynamicSqlSource,不是返回RawSqlSource

/**
 * @Author df
 * @Description: 动态SQL源码
 * @Date 2023/12/22 11:20
 */
// step-15新增
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(parameterType);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
    }


        // 解析脚本里的动态节点
     // 更改判断是否动态调用不同的SqlSource
    public SqlSource parseScriptNode() {
        // step-15修改
        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;
    }
}

4.测试准备

dao层:

public interface IActivityDao {

    Activity queryActivityById(Activity activity);

}

?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="cn.bugstack.mybatis.test.dao.IActivityDao">
    <resultMap id="activityMap" type="cn.bugstack.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="cn.bugstack.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>

?单元测试

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    private SqlSession sqlSession;

    @Before
    public void init() throws IOException {
        // 1. 从SqlSessionFactory中获取SqlSession
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
        sqlSession = sqlSessionFactory.openSession();
    }

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

执行单元测试,结果

文章来源:https://blog.csdn.net/dfBeautifulLive/article/details/135223850
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。