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

2017-10-22  本文已影响183人  小胖0_0

作者:艾志谋

日期: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
上一篇下一篇

猜你喜欢

热点阅读