HAP框架中batchUpdate方法分析与使用经验
作者:艾志谋
日期: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>
<bind name="countryname_bind" value='@java.util.UUID@randomUUID().toString().replace("-", "")'/>
INSERT INTO country_u
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
countryname,
<if test="countrycode != null">countrycode,</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">#{id,javaType=java.lang.Integer},</if>
<if test="countryname != null">#{countryname,javaType=java.lang.String},</if>
<if test="countryname == null">#{countryname_bind,javaType=java.lang.String},</if>
<if test="countrycode != null">#{countrycode,javaType=java.lang.String},</if>
</trim>
</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方法注意事项
- 注意不要让传入的dto的
__status
属性为空,否则会报空指针异常 - 注意区分updateByPrimaryKeySelective方法和updateByPrimaryKey方法的区别。默认调用updateByPrimaryKeySelective方法,如果需要调用updateByPrimaryKey方法,则需要重写useSelectiveUpdate方法,更改其返回值为false
- 如果该方法有问题,可以查看动态生成的sql是否能满足业务需求,若不能,则需要自行定制sql