首页 > 数据库 >Mybatis中SqlNode的组合模式

Mybatis中SqlNode的组合模式

时间:2023-06-15 12:33:05浏览次数:57  
标签:String 组合 private SqlNode context SQL Mybatis null final


组合( 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>

这其中的<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的实现类如下

Mybatis中SqlNode的组合模式_ide

其中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, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
    //使用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的内部类,继承于DynamicContext
  FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  //调用子节点的apply()方法进行解析,注意收集SQL语句的是filteredDynamicContext
  boolean 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;

标签:String,组合,private,SqlNode,context,SQL,Mybatis,null,final
From: https://blog.51cto.com/u_16145034/6486230

相关文章

  • 第八章--FusionCharts Free和组合图XML
    时间:2009-01-1222:23      作者:道长AIEQQ百度POCOYahoo新浪365Key天极和讯博拉Live奇客鲜果收客饭否叽歪  xAxisName='Month' showValues='0' de......
  • MyBatis-Plus的BaseMapper与IService
    mybatis-plus提供两种包含预定义增删改查操作的接口:com.baomidou.mybatisplus.core.mapper.BaseMappercom.baomidou.mybatisplus.extension.service.IService刚开始有点疑惑为什么要设计俩个接口用来增删改查BaseMapper和IService的方法有很多类似的功能,但是方法名不同对比......
  • mybatisplus总结
    mybatisplus和springboot的集成导入依赖<!--spring-boot-web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dep......
  • mybatis 处理in 1000以上sql
    展开查看点击查看代码updateT_XSGL_XSXX_XJXXxjxxsetxjxx.XWZSBH=null,xjxx.ZHXGR=#{userId},xjxx.ZHXGSJ=sysdatewhereexists(select1fromT_BYGL_XSBYJGbyjgwherebyjg.GDMCisnullandbyjg.XS_ID=xjxx.XS_ID)and(xjxx.......
  • Mybatis框架
    SpringSpringMVC接下来学习controllerspringmvc框架serviceSpring框架dao层用 Mybatis框架什么是MyBatisMyBatis是一款优秀的持久层框架它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以......
  • 使用Mybatis生成树形菜单-适用于各种树形场景
    开发中我们难免会遇到各种树形结构展示的场景。比如用户登录系统后菜单的展示,某些大型购物网站商品的分类展示等等,反正开发中会遇到各种树形展示的功能,这些功能大概处理的思路都是一样的,所以本文就总结一下树形结构的代码生成,在开发的时候套用这种结构就可以了。好了正文开始,首先......
  • MyBatisPlus_动态更新
    场景:项目整合了mybatisplus,进行update更新,前端传值为空时,数据库也进行了更新,导致原来的值丢失。解决方案:在实体类上使用@TableField注解/*联系人手机号码*/@TableField(updateStrategy=FieldStrategy.NOT_EMPTY)privateStringphone;这样,当前端传过来的phone参数为......
  • 调题时出现的问题 in 『组合数学』
    (递推计算组合数)P4071[SDOI2016]排列计数吐个槽先:没啥好吐槽的,就是我自己傻掉了.Orz.这题的式子非常水.在\(n\)个数里面选\(m\)个固定,即\(\mathrm{C}_n^m\).再把剩下的\(n-m\)个数错序排列,即\(h(n-m)\).因为只有上面的\(m\)个数才能满足\(i=a_i\)......
  • 基于SpringBoot+MyBatis+Thymeleaf的学生管理系统搭建
    学生管理系统Maven工程搭建【步骤】:打开IDEA工具,选择创建一个新工程。选择SpringInitializr,点击Next按钮。大家也可以通过Spring提供的在线创建的方式创建工程,访问(https://start.spring.io),然后将创建后的工程代码zip包解压后,使用IDEA导入工程。这种方式不在本文描述......
  • mybatis批量插入的四种方式
    一、循环插入publicvoidinsert(List<User>userList){userList.forEach(user->userDao.insert(user));}<insertid="insert">INSERTINTO`demo`.`user`(`username`,`address`,`remark`,`age`,`create_time`)VALUES(#{u......