美文网首页
HAP框架中batchUpdate方法分析与使用经验

HAP框架中batchUpdate方法分析与使用经验

作者: 小胖0_0 | 来源:发表于2017-10-22 22:43 被阅读183次

    作者:艾志谋

    日期:2017/10/22

    版本:1.0

    1. batchUpdate方法说明

    List<T> batchUpdate(IRequest request, List<T> list)是HAP项目中十分常用的一个方法,这个方法集成了对DTO的insert、update和delete三个操作,HAP将其封装在IBaseService接口中,而我们所有的自定义Service接口都是默认继承自IBaseService接口的,在Controller中我们可以非常方便的调用Service中的这个方法对实体类进行批量的增、删、改操作。

    2. BatchUpdate方法简单分析

    2.1 IBaseService接口

    在HAP代码生成器在对应的表为我们自动生成的service接口中我们可以看到,所有的service接口都是默认继承自IBaseService接口的(当然,我们也可以手动继承IBaseService接口),如下代码所示:

    public interface IGxpCusHeaderService extends IBaseService<GxpCusHeader>, ProxySelf<IGxpCusHeaderService>{
      ......
    }
    

    我们可以在IBaseService接口中看到batchUpdate方法的定义如下:

    List<T> batchUpdate(IRequest request, @StdWho List<T> list);
    

    方法的定义非常简单,需要一个request参数和一个List集合,集合中可以放入任何DTO,返回值仍然是一个list集合。

    2.2 BaseServiceImpl实现类

    在BaseServiceImpl实现类中,我们可以看到这个方法的实现实现如下:

    /**
         * this method assume the object in list is BaseDTO.
         * 
         * @param request
         *            requestContext
         * @param list
         *            dto list
         * @return the list
         */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<T> batchUpdate(IRequest request, List<T> list) {
      IBaseService<T> self = ((IBaseService<T>) AopContext.currentProxy());
      for (T t : list) {
        switch (((BaseDTO) t).get__status()) {
          case DTOStatus.ADD:
            self.insertSelective(request, t);
            break;
          case DTOStatus.UPDATE:
            if (useSelectiveUpdate()) {
              self.updateByPrimaryKeySelective(request, t);
            } else {
              self.updateByPrimaryKey(request, t);
            }
            break;
          case DTOStatus.DELETE:
            self.deleteByPrimaryKey(t);
            break;
          default:
            break;
        }
      }
      return list;
    }
    

    可以看到这个batchUpdate()方法中根据dto的__status属性值将dto的操作分为三种:add、update和delete,分别再调用对用的方法进行实际操作。

    这段代码中,DTOStatus是一个final类,里面定义了三个常量,用来记录前台传过来的对dto的操作状态,代码如下:

    public final class DTOStatus {
        
        private DTOStatus() {
        }
        
        /**
         * Liger UI 记录状态 - 新增.
         */
        public static final String ADD = "add";
        
        /**
         * Liger UI 记录状态 - 更新.
         */
        public static final String UPDATE = "update";
        
        /**
         * Liger UI 记录状态 - 删除.
         */
        public static final String DELETE = "delete";
    }
    

    实际上这个方法中有一个十分不安全的地方,那就是将((BaseDTO) t).get__status()传入到switch中作为参数之前没有检查其值是否为null,如果将null传入switch中作为参数会造成空指针异常。这里需要抽空和HAP研发组反馈一下。

    虽然Java7中新增了switch支持String类型的新特征,但是在java底层,switch中还是只能使用与整型相兼容的类型,比如byte、short、char以及int。java7中switch对String类型的支持是在编辑器层面实现的,虽然开发者在java源代码中使用了String类型,但是编译器在编译的时候会根据源代码的含义进行转换,将字符串类型转换成与整数类型兼容的格式,主要是使用equals()和hashCode()方法实现的。那么这个时候如果我们传入的是一个null,null调用equals()和hashCode()方法就会报空指针异常。

    更多关于Java7中switch-case支持String类型的实现细节可以参考这篇文章:Java中字符串switch的实现细节

    因此要使用这个方法,必须保证传入的dto的__status属性值不能为空,否者就会报空指针异常。

    2.3 insertSelective方法

    我们可以看到这个方法中是通过调用insertSelective(request, t)方法对dto进行插入操作的,这个方法会根据所传入的dto自动将dto中不为null的值插入到对应的table中,其方法的实现源码在BaseInsertProvider这个类中,代码如下:

    /**
         * 插入不为null的字段,这段代码比较复杂,这里举个例子
         * CountryU生成的insertSelective方法结构如下:
         * <pre>
         &lt;bind name="countryname_bind" value='@java.util.UUID@randomUUID().toString().replace("-", "")'/&gt;
         INSERT INTO country_u
         &lt;trim prefix="(" suffix=")" suffixOverrides=","&gt;
         &lt;if test="id != null"&gt;id,&lt;/if&gt;
         countryname,
         &lt;if test="countrycode != null"&gt;countrycode,&lt;/if&gt;
         &lt;/trim&gt;
         VALUES
         &lt;trim prefix="(" suffix=")" suffixOverrides=","&gt;
         &lt;if test="id != null"&gt;#{id,javaType=java.lang.Integer},&lt;/if&gt;
         &lt;if test="countryname != null"&gt;#{countryname,javaType=java.lang.String},&lt;/if&gt;
         &lt;if test="countryname == null"&gt;#{countryname_bind,javaType=java.lang.String},&lt;/if&gt;
         &lt;if test="countrycode != null"&gt;#{countrycode,javaType=java.lang.String},&lt;/if&gt;
         &lt;/trim&gt;
         </pre>
         * 这段代码可以注意对countryname的处理
         *
         * @param ms
         * @return
         */
    public String insertSelective(MappedStatement ms) {
      Class<?> entityClass = getEntityClass(ms);
      StringBuilder sql = new StringBuilder();
      //获取全部列
      Set<EntityColumn> columnList = EntityHelper.getColumns(entityClass);
      //Identity列只能有一个
      Boolean hasIdentityKey = false;
      //先处理cache或bind节点
      for (EntityColumn column : columnList) {
        if (!column.isInsertable()) {
          continue;
        }
        if (StringUtil.isNotEmpty(column.getSequenceName())) {
          //sql.append(column.getColumn() + ",");
        } else if (column.isIdentity()) {
          //这种情况下,如果原先的字段有值,需要先缓存起来,否则就一定会使用自动增长
          //这是一个bind节点
          sql.append(SqlHelper.getBindCache(column));
          //如果是Identity列,就需要插入selectKey
          //如果已经存在Identity列,抛出异常
          if (hasIdentityKey) {
            //jdbc类型只需要添加一次
            if (column.getGenerator() != null && column.getGenerator().equals("JDBC")) {
              continue;
            }
            throw new RuntimeException(ms.getId() + "对应的实体类" + entityClass.getCanonicalName() + "中包含多个MySql的自动增长列,最多只能有一个!");
          }
          //插入selectKey
          newSelectKeyMappedStatement(ms, column);
          hasIdentityKey = true;
        } else if (column.isUuid()) {
          //uuid的情况,直接插入bind节点
          sql.append(SqlHelper.getBindValue(column, getUUID()));
        }
      }
      sql.append(SqlHelper.insertIntoTable(entityClass, tableName(entityClass)));
      sql.append("<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">");
      for (EntityColumn column : columnList) {
        if (!column.isInsertable()) {
          continue;
        }
        if(column.isIdentity()&&getIDENTITY().equals("JDBC")) {
          continue;
        }
        if (StringUtil.isNotEmpty(column.getSequenceName()) || column.isIdentity() || column.isUuid()) {
          sql.append(column.getColumn() + ",");
        } else {
          sql.append(SqlHelper.getIfNotNull(column, column.getColumn() + ",", isNotEmpty()));
        }
      }
      sql.append("</trim>");
      sql.append("<trim prefix=\"VALUES(\" suffix=\")\" suffixOverrides=\",\">");
      for (EntityColumn column : columnList) {
        if (!column.isInsertable()) {
          continue;
        }
        if(column.isIdentity()&&getIDENTITY().equals("JDBC")) {
          continue;
        }
        //优先使用传入的属性值,当原属性property!=null时,用原属性
        //自增的情况下,如果默认有值,就会备份到property_cache中,所以这里需要先判断备份的值是否存在
        if (column.isIdentity()) {
          sql.append(SqlHelper.getIfCacheNotNull(column, column.getColumnHolder(null, "_cache", ",")));
        } else {
          //其他情况值仍然存在原property中
          sql.append(SqlHelper.getIfNotNull(column, column.getColumnHolder(null, null, ","), isNotEmpty()));
        }
        //当属性为null时,如果存在主键策略,会自动获取值,如果不存在,则使用null
        //序列的情况
        if (StringUtil.isNotEmpty(column.getSequenceName())) {
          sql.append(SqlHelper.getIfIsNull(column, getSeqNextVal(column) + " ,", isNotEmpty()));
        } else if (column.isIdentity()) {
          sql.append(SqlHelper.getIfCacheIsNull(column, column.getColumnHolder() + ","));
        } else if (column.isUuid()) {
          sql.append(SqlHelper.getIfIsNull(column, column.getColumnHolder(null, "_bind", ","), isNotEmpty()));
        }
      }
      sql.append("</trim>");
      return sql.toString();
    }
    
    

    这个方法会根据传入的具体的dto动态的拼接对应的sql,插入对应的table中,需要注意的是,这个方法插入的是所有不为null的值,为null的值会使用数据库设置的默认值。这段代码比较复杂,里面的一些东西我暂时也不懂,后续有时间再继续研究。

    2.4 updateByPrimaryKeySelective方法

    batchUpdate方法中为更新设置了两个方法,一个是updateByPrimaryKeySelective(request, t)方法,一个是updateByPrimaryKey(request, t)方法,通过一个方法useSelectiveUpdate()决定具体调用哪一个方法去执行更新,下面我们一个一个来分析。首先看updateByPrimaryKeySelective(request, t)方法。

    这个方法的具体实现在BaseUpdateProvider这个类中,这个方法也是一个动态更新的方法,会根据传入的具体的dto参数去更新对应table中的字段,代码如下:

    /**
         * 通过主键更新不为null的字段
         *
         * @param ms
         * @return
         */
    public String updateByPrimaryKeySelective(MappedStatement ms) {
      Class<?> entityClass = getEntityClass(ms);
      StringBuilder sql = new StringBuilder();
      sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
      sql.append(SqlHelper.updateSetColumns(entityClass, null, true, isNotEmpty()));
      sql.append(SqlHelper.wherePKColumns(entityClass));
      appendObjectVersionNumber(sql, entityClass);
      return sql.toString();
    }
    

    需要注意的是这个方法是根据主键更新不为null的字段,传入的dto中为null的参数不会更新。这个方法代码比较短,也是调用了很多更加底层的方法实现的,后期需要更加深入的了解更底层的实现方法。

    2.5 updateByPrimaryKey方法

    这个方法也是在BaseUpdateProvider中定义的,具体代码如下:

    
    /**
         * 通过主键更新全部字段
         *
         * @param ms
         */
    public String updateByPrimaryKey(MappedStatement ms) {
      Class<?> entityClass = getEntityClass(ms);
      StringBuilder sql = new StringBuilder();
      sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
      sql.append(SqlHelper.updateSetColumns(entityClass, null, false, false));
      sql.append(SqlHelper.wherePKColumns(entityClass));
      appendObjectVersionNumber(sql, entityClass);
      return sql.toString();
    }
    

    需要注意的是这个方法是通过主键更新全部字段,意思是如果传入的dto中如果有为null的字段,会一并将数据库中对应的字段值也更新为null

    2.6 useSelectiveUpdate方法

    这个方法非常简单,只是返回一直true/false,但是很重要,也很容易被忽略,这个方法直接定义在BaseServiceImpl接口中,具体代码如下:

    /**
         * 默认 true,表示在 batchUpdate 中,更新操作,使用updateByPrimaryKeySelective(只更新不为 null
         * 的字段)。
         * 若返回 false,则使用 updateByPrimaryKey(更新所有字段)
         * 
         * @return
         */
    protected boolean useSelectiveUpdate() {
      return true;
    }
    
    

    方法的注释里面写得很清楚,这个方法决定batchUpdate更新中执行updateByPrimaryKeySelective方法还是updateByPrimaryKey方法,如果需要更新所有字段,则需要重写这个方法,将其返回值改为false

    2.7 deleteByPrimaryKey方法

    这是batchUpdate方法中执行删除操作的方法,改方法定义在BaseDeleteProvider类中,代码如下:

    /**
         * 通过主键删除
         *
         * @param ms
         */
    public String deleteByPrimaryKey(MappedStatement ms) {
      final Class<?> entityClass = getEntityClass(ms);
      StringBuilder sql = new StringBuilder();
      sql.append(SqlHelper.deleteFromTable(entityClass, tableName(entityClass)));
      sql.append(SqlHelper.wherePKColumns(entityClass));
      BaseUpdateProvider.appendObjectVersionNumber(sql, entityClass);
      return sql.toString();
    }
    

    这个方法比较简单,就是根据主键删除数据库中指定的记录。也是动态生成的sql,后期需要对这个动态sql进行更加深入的了解和学习。

    3. batchUpdate方法注意事项

    1. 注意不要让传入的dto的__status属性为空,否则会报空指针异常
    2. 注意区分updateByPrimaryKeySelective方法和updateByPrimaryKey方法的区别。默认调用updateByPrimaryKeySelective方法,如果需要调用updateByPrimaryKey方法,则需要重写useSelectiveUpdate方法,更改其返回值为false
    3. 如果该方法有问题,可以查看动态生成的sql是否能满足业务需求,若不能,则需要自行定制sql

    相关文章

      网友评论

          本文标题:HAP框架中batchUpdate方法分析与使用经验

          本文链接:https://www.haomeiwen.com/subject/kjonuxtx.html