Mybatis中SqlNode的组合模式
组合( Composite )模式就是把对象组合成树形结构,以表示“部分-整体”的层次结构,用户可以像处理一个简单对象一样来处理一个复杂对象,从而使得调用者无需了解复杂元素的内部结构。
组合模式中的角色有:
抽象组件(容器):定义了树形结构中所有类的公共行为,例如add(),remove()等方法。
树叶:最终实现类,没有子类。
树枝:有子类的管理类,并通过管理方法调用其管理的子类的相关操作。
调用者:通过容器接口操作整个树形结构。
具体组合模式的例子可以参考 设计模式整理
现在我们来说一下SqlNode是什么,来看这么一段配置文件
<select id="findByGameTypeCount" resultType="java.lang.Long"> select count(*)from betdetails a inner join UserBetOrder b on a.orderId = b.id <where> <if test="gameType != null and gameType > 0"> a.gameType = #{gameType} and </if> <if test="currDrawno != null"> b.currentDrawno = #{currDrawno} and </if> <if test="orderId != null and orderId > 0"> a.orderId = #{orderId} and </if> <if test="status != null and status >= 0"> a.status = #{status} and </if> <if test="userId != null and userId > 0"> b.userId = #{userId} and </if> <if test="start != null"> a.createTime >= #{start} and </if> <if test="end != null"> a.createTime <= #{end} and </if> 1 = 1 </where></select>
<insert id="insertBetdetailsByBatch" parameterType="java.util.List"> insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values <foreach collection="list" item="item" index="index" separator=","> (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName}) </foreach></insert>
这其中的<if><where><foreach>节点就是SqlNode节点,SqlNode是一个接口,代表着组合模式中的容器。只要是有SqlNode,那就代表着一定是一个动态的SQL,里面就有可能会有参数#{}
public interface SqlNode {//SqlNode接口中定义的唯一方法,该方法会根据用户传入的实参,解析该SqlNode所记录的动态SQL节点,并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到//DynamicContext.sqlBuilder中保存//当SQL节点下的所有SqlNode完成解析后,就可以从DynamicContext中获取一条动态生成的完整的SQL语句 boolean apply(DynamicContext context);}
我们先来看一下DynamicContext是什么,它的核心字段如下
private final ContextMap bindings; //参考上下文 //在SqlNode解析动态SQL时,会将解析后的SQL语句片段添加到该属性中保存,最终拼凑出一条完成的SQL语句private final StringBuilder sqlBuilder = new StringBuilder();
ContextMap是一个内部类,继承于HashMap,重写了get方法
static class ContextMap extends HashMap<String, Object> { private static final long serialVersionUID = 2977601501966151582L; //将用户传入的参数封装成MetaObject对象(类实例中检查类的属性是否包含getter,setter方法) private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject; } @Override public Object get(Object key) {String strKey = (String) key;//如果ContextMap中已经包含了该key,则直接返回if (super.containsKey(strKey)) { return super.get(strKey); }//如果不包含该key,从parameterMetaObject中查找对应属性if (parameterMetaObject != null) { // issue #61 do not modify the context when reading return parameterMetaObject.getValue(strKey); } return null; } }
public void appendSql(String sql) { sqlBuilder.append(sql); sqlBuilder.append(" ");}
SqlNode的实现类如下
其中MixedSqlNode是树枝,TextSqlNode是树叶....
我们先来看一下TextSqlNode,TextSqlNode表示的是包含${}占位符的动态SQL节点。它的接口实现方法如下
@Overridepublic boolean apply(DynamicContext context) {//将动态SQL(带${}占位符的SQL)解析成完成SQL语句的解析器,即将${}占位符替换成实际的变量值GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));//将解析后的SQL片段添加到DynamicContext中 context.appendSql(parser.parse(text)); return true;}
BindingTokenParser是TextNode中定义的内部类,继承了TokenHandler接口,它的主要作用是根据DynamicContext.bindings集合中的信息解析SQL语句节点中的${}占位符。
private DynamicContext context;
private Pattern injectionFilter; //需要匹配的正则表达式
@Overridepublic String handleToken(String content) {//获取用户提供的实参Object parameter = context.getBindings().get("_parameter");//如果实参为null if (parameter == null) {//将参考上下文的value key设为null context.getBindings().put("value", null);//如果实参是一个常用数据类型的类(Integer.class,String.class,Byte.class等等) } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {//将参考上下文的value key设为该实参 context.getBindings().put("value", parameter); }//通过OGNL解析参考上下文的值Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"//检测合法性 checkInjection(srtValue); return srtValue;}
private void checkInjection(String value) { if (injectionFilter != null && !injectionFilter.matcher(value).matches()) { throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern()); } }
在OgnlCache中,对原生的OGNL进行了封装。OGNL表达式的解析过程是比较耗时的,为了提高效率,OgnlCache中使用了expressionCashe字段(ConcurrentHashMap<String,Object>类型)对解析后的OGNL表达式进行缓存。为了说明OGNL,我们先来看一个例子
@Data@ToStringpublic class User { private int id; private String name;}
public class OGNLDemo { public void testOgnl1() throws OgnlException {OgnlContext context = new OgnlContext(); context.put("cn","China"); String value = (String) context.get("cn"); System.out.println(value); User user = new User(); user.setId(100); user.setName("Jack"); context.put("user",user); Object u = context.get("user"); System.out.println(u); Object ognl = Ognl.parseExpression("#user.id"); Object value1 = Ognl.getValue(ognl,context,context.getRoot()); System.out.println(value1); User user1 = new User(); user1.setId(200); user1.setName("Mark"); context.setRoot(user1); Object ognl1 = Ognl.parseExpression("id"); Object value2 = Ognl.getValue(ognl1,context,context.getRoot()); System.out.println(value2); Object ognl2 = Ognl.parseExpression("@@floor(10.9)"); Object value3 = Ognl.getValue(ognl2, context, context.getRoot()); System.out.println(value3); } public static void main(String[] args) throws OgnlException {OGNLDemo demo = new OGNLDemo(); demo.testOgnl1(); } }
运行结果:
China
User(id=100, name=Jack)
100
200
10.0
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
public static Object getValue(String expression, Object root) { try {//创建OgnlContext对象Map<Object, OgnlCla***esolver> context = Ognl.createDefaultContext(root, new OgnlCla***esolver());//使用OGNL执行expression表达式return Ognl.getValue(parseExpression(expression), context, root); } catch (OgnlException e) { throw new BuilderException("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;}
StaticTextSqlNode很简单,就是直接返回SQL语句
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; }}
IfSqlNode是解析<if>节点,字段含义如下
//用于解析<if>节点的test表达式的值 private final ExpressionEvaluator evaluator; //记录<if>节点中test表达式private final String test; //记录了<if>节点的子节点private final SqlNode contents;
接口方法如下
@Overridepublic boolean apply(DynamicContext context) {//检测test属性中记录的表达式 if (evaluator.evaluateBoolean(test, context.getBindings())) {//如果test表达式为true,则执行子节点的apply()方法 contents.apply(context); return true; //返回test表达式的结果为true } return false; //返回test表达式的结果为false}
在ExpressionEvaluator中
public boolean evaluateBoolean(String expression, Object parameterObject) {//用OGNL解析expression表达式Object value = OgnlCache.getValue(expression, parameterObject);//处理Boolean类型 if (value instanceof Boolean) { return (Boolean) value; }//处理数字类型 if (value instanceof Number) { return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; } return value != null;}
TrimSqlNode会根据子节点的解析结果,添加或删除响应的前缀或后缀,比如有这么一段配置
<insert id="insertNotNullBetdetails" parameterType="com.cloud.model.game.Betdetails"> insert into betdetails <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null">id,</if> <if test="orderId != null">orderId,</if> <if test="actorIndex != null">actorIndex,</if> <if test="ballIndex != null">ballIndex,</if> <if test="ballValue != null">ballValue,</if> <if test="betAmount != null">betAmount,</if> <if test="createTime != null">createTime,</if> <if test="rate1 != null">rate1,</if> <if test="rate2 != null">rate2,</if> <if test="rate3 != null">rate3,</if> <if test="gameType != null">gameType,</if> <if test="status != null">status,</if> <if test="betResult != null">betResult,</if> <if test="awardAmount != null">awardAmount,</if> <if test="ballName != null">ballName,</if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null">#{id},</if> <if test="orderId != null">#{orderId},</if> <if test="actorIndex != null">#{actorIndex},</if> <if test="createTime != null">#{createTime},</if> <if test="ballIndex != null">#{ballIndex},</if> <if test="ballValue != null">#{ballValue},</if> <if test="betAmount != null">#{betAmount},</if> <if test="rate1 != null">#{rate1},</if> <if test="rate2 != null">#{rate2},</if> <if test="rate3 != null">#{rate3},</if> <if test="gameType != null">#{gameType},</if> <if test="status != null">#{status},</if> <if test="betResult != null">#{betResult},</if> <if test="awardAmount != null">#{awardAmount},</if> <if test="ballName != null">#{ballName},</if> </trim></insert>
TrimSqlNode中字段含义如下
private final SqlNode contents; //该<trim>节点的子节点private final String prefix; //记录了前缀字符串(为<trim>节点包裹的SQL语句添加的前缀)private final String suffix; //记录了后缀字符串(为<trim>节点包裹的SQL语句添加的后缀) //如果<trim>节点包裹的SQL语句是空语句,删除指定的前缀,如whereprivate final List<String> prefixesToOverride; //如果<trim>节点包裹的SQL语句是空语句,删除指定的后缀,如逗号private final List<String> suffixesToOverride;
它的接口方法如下
@Overridepublic boolean apply(DynamicContext context) {//创建FilteredDynamicContext对象,FilteredDynamicContext是TrimSqlNode的内部类,继承于DynamicContextFilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);//调用子节点的apply()方法进行解析,注意收集SQL语句的是filteredDynamicContextboolean result = contents.apply(filteredDynamicContext);//处理前缀和后缀 filteredDynamicContext.applyAll(); return result;}
FilteredDynamicContext的字段属性含义如下
private DynamicContext delegate; //底层封装的DynamicContext对象private boolean prefixApplied; //是否已经处理过前缀private boolean suffixApplied; //是否已经处理过后缀private StringBuilder sqlBuffer; //用于记录子节点解析后的结果
FilteredDynamicContext的applyAll()方法
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片段添加到DynamicContext的StringBuilder中 delegate.appendSql(sqlBuffer.toString());}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { //如果还没有处理过前缀 prefixApplied = true; //更新为已处理 if (prefixesToOverride != null) { //如果需要删除的前缀列表不为null//遍历该前缀列表 for (String toRemove : prefixesToOverride) {//如果<trim>子节点收集上来的SQL语句以该前缀开头 if (trimmedUppercaseSql.startsWith(toRemove)) {//从<trim>子节点收集上来的StringBuilder中删除该前端sql.delete(0, toRemove.trim().length()); break; }}}//如果有前缀字符串(比如说"("),将前缀字符串插入StringBuilder最前端 if (prefix != null) {sql.insert(0, " "); sql.insert(0, prefix); }} }
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { if (!suffixApplied) { //如果还没有处理过后缀 suffixApplied = true; //更新为已处理后缀 if (suffixesToOverride != null) { //如果需要处理的后缀列表不为null//遍历该后缀列表 for (String toRemove : suffixesToOverride) {//如果从<trim>子节点收集上来的SQL语句以该后缀结尾 if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {//获取该后缀的起始位置 int start = sql.length() - toRemove.trim().length();//获取该后缀的终止位置int end = sql.length();//从<trim>子节点收集上来的StringBuilder中删除该后端 sql.delete(start, end); break; }}}//如果有后缀字符串(比如说")"),将前缀字符串拼接上StringBuilder最后端 if (suffix != null) {sql.append(" "); sql.append(suffix); }} }
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); }}
public class SetSqlNode extends TrimSqlNode { private static List<String> suffixList = Arrays.asList(","); public SetSqlNode(Configuration configuration,SqlNode contents) { super(configuration, contents, "SET", null, null, suffixList); }}
ForEachSqlNode,在动态SQL语句中,通常需要对一个集合进行迭代,Mybatis提供了<foreach>标签实现该功能。在使用<foreach>标签迭代集合时,不仅可以使用集合的元素和索引值,还可以在循环开始之前或结束之后添加指定的字符串,也允许在迭代过程中添加指定的分隔符。配置样例如下
<insert id="insertBetdetailsByBatch" parameterType="java.util.List"> insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values <foreach collection="list" item="item" index="index" separator=","> (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName}) </foreach></insert>
ForEachSqlNode中各个字段含义如下:
private final ExpressionEvaluator evaluator;private final String collectionExpression;private final SqlNode contents;private final String open;private final String close;private final String separator;private final String item;private final String index;private final Configuration configuration;
转载于:https://blog.51cto.com/14009535/2376382
Mybatis中SqlNode的组合模式相关推荐
- 在王者荣耀角度下分析面向对象程序设计B中23种设计模式之组合模式
· 组合模式在王者荣耀中的应用 · 一.简述 王者荣耀有非常多的英雄,而且每一个不同的英雄也有其自己对应的皮肤.玩家可以根据需要购买不同的英雄皮肤,体验不同英雄皮肤在对局中的感受. 在本实例中,根据组 ...
- (九)Java架构师成长之路-设计模式:组合模式
一.定义 组合模式也叫整体-部分模式,目的是为了实现整体和部分都有一组相同的操作方法. 属于结构型模式 注意区别一下组合和聚合的区别: 组合:各部分组织结构有相同的生命周期,例如人的各个器官和人,人死 ...
- 1、【设计模式】组合模式
java设计模式之组合模式 [学习难度:★★★☆☆,使用频率:★★★★☆] 树形结构在软件中随处可见,例如操作系统中的目录结构.应用软件中的菜单.办公系统中的公司组织结构等等,如何运用面向对象的方式 ...
- 组合模式的安全模式与透明模式
转载自 树形结构的处理--组合模式(四) 1 透明组合模式与安全组合模式 通过引入组合模式,Sunny公司设计的杀毒软件具有良好的可扩展性,在增加新的文件类型时,无须修改现有类库代码,只需增加一个新的 ...
- python 设计模式之组合模式Composite Pattern
#引入一 文件夹对我们来说很熟悉,文件夹里面可以包含文件夹,也可以包含文件. 那么文件夹是个容器,文件夹里面的文件夹也是个容器,文件夹里面的文件是对象. 这是一个树形结构 咱们生活工作中常用的一种结构 ...
- 设计模式学习笔记(十一)-组合模式
树形结构在软件中随处可见,例如操作系统中的目录结构.应用软件中的菜单.办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题,组合模式通过一种巧妙的设计方案使得 ...
- 设计模式GOF23之-------------------结构型模式(适配器模式、代理模式、桥接模式、装饰模式、组合模式、外观模式、享元模式)
一 结构型模式 二 适配器模式 下面我将用代码模拟键盘usb接口和ps/2的转接器 的适配器过程: 首先定义客户需求: package GOF23;public interface Target {v ...
- 树形结构的处理——组合模式(三)
11.3 完整解决方案 为了让系统具有更好的灵活性和可扩展性,客户端可以一致地对待文件和文件夹,Sunny公司开发人员使用组合模式来进行杀毒软件的框架设计,其基本结构如图11-5所示: 图11-5 ...
- 树形结构的处理——组合模式(四)
11.4 透明组合模式与安全组合模式 通过引入组合模式,Sunny公司设计的杀毒软件具有良好的可扩展性,在增加新的文件类型时,无须修改现有类库代码,只需增加一个新的文件类作为AbstractFile ...
最新文章
- CSS 布局:40个教程、技巧、例子和最佳实践
- struts2 实现多文件限制上传
- linux信号检查参数相同,linux信号函数
- 牛客网在线编程之“字符串分割”
- VTK:方向标记用法实战
- 拼装sql_SQL优化最干货总结(2020最新版)
- 换光纤猫 ZXA10 F420
- SAP Kyma组件一览
- 在阿里、腾讯、美团工作有什么区别?
- docker php 一键部署_Java开发提升十倍生产力:Idea远程一键部署springboot到Docker
- Oracle历史记录
- 9;XHTML 多媒体
- opengl 关于glGenBuffers函数没有定义
- 如何在 Mac 上输入带重音符的字符?
- 关于软件设计使用中一些的原则简述
- CHD6.3环境中,集中kylin3.0,错误解决.NoClassDefFoundError: org/apache/commons/configuration/ConfigurationE
- “数仓”-MPP与 MR的区别
- gdi win7奔溃_Win7系统细致核心图形架构的操作方法
- 阿里云支持Windows Server 2008操作系统镜像
- 高危预警!移动设备安全面临的5大新型威胁
热门文章
- CentOS使用 Crontab定时任务清理程序日志
- spring学习--基于注解方式创建对象AOP
- 轻量级java开发工具_C++主流开发工具推荐
- python海龟图画龙珠_DeepOps的Python小笔记-天池龙珠计划-Python训练营-Task 02:DAY5
- 命令 结构_关于Oracle常用查看表结构命令总结分享
- 本篇文章使用vue结合element-ui开发tab切换vue的不同组件,每一个tab切换的都是一个新的组件。
- php h5视频录制上传,基于koa的h5视频录制异步上传
- php 父进程id,PHP pcntl_fork创建进程,复制父进程内存空间上下文
- 街篮服务器维修时间,全新花式技能系统 《街头篮球》春节前夕版本上线
- html捉虫游戏,街机游戏