背景
项目中使用了paoding-rose作为开发框架,该框架作为国产的一个十分优秀的框架,在Jade方面处理的也非常好,但是在实际的使用过程中,发现了一个很有意思的问题,在使用Delete SQL语句批量删除数据时,当传参进去只有一个参数时,会因为返回类型的不知道导致抛异常。
这里还是先把解决方案说了好了,升级到最新2.0以上版本即可(我们使用的是比较老的1.2.2版本)
然后下面为了备忘,特别记录一下这个蛮有意思的问题。
Git上可以参考这个Issue
问题描述
问题主要体现为,偶然性会报ClassCastException错误,具体表现形式如下:
java.lang.ClassCastException: [I cannot be cast to java.lang.Integer
at com.sun.proxy.$Proxy51.deleteRecommendListByIDs(Unknown Source)
at com.xx.service.xxxService.generateDefaultRecommendList(xxxService.java:214)
at com.xx.service.xxxService.main(xxxService.java:951)
其错误来源于通过@SQL的注解实现了SQL语句的执行地方,具体的SQL如下:
@SQL(" DELETE FROM videolist_home_recommend "
+" WHERE ID IN ( :delRecords ) ")
public Integer deleteRecommendListByIDs(@SQLParam("delRecords") List<Integer> delRecords);
以上就是这个错误产生的基本现象,但是有意思的该问题并非一定会产生,根据详细的测试和分析发现,只有在传参List中元素个数大于1个以上时,会导致该错误的发生。
原因分析
- 首先分析了这个现象以后觉得可能是传参进入的问题,导致了List转化存在的问题,但是后来请教了同事,其实rose这里和数据库中一般SQL语句的返回是不一致的,数据库中返回的是影响的行数,但是Rose中是一个是否更新成功的int[],其中1代表处理成功,0代表处理失败(具体为什么这么处理后面会分析到)。还有这里有个需要反省的就是,对getName()这个方法不熟悉,其实JVM已经告诉了我们问题点,出问题的类型是"[I",只不过没反应过来,括号哭~
String java.lang.Class.getName()
Returns the name of the entity (class, interface, array class, primitive type, or void) represented by this Class object, as a String.
……
If this class object represents a class of arrays, then the internal form of the name consists of the name of the element type preceded by one or more '[' characters representing the depth of the array nesting. The encoding of element type names is as follows:
……
Element Type
boolean Z
byte B
……
int I
long J
short S
所以之后根据这个返回值将SQL方法的返回值替换成为int[]类型,然而有趣的是这次反而在参数list只有一个值时报错了,于是进一步debug到源码中去看了一下(由于github上找不到1.2.2版本的内容,又找了好久source文件)
这里就发现rose jade中关于这个处理很有意思:
- 在rose中,SQL语句只有两种类型:READ和WRITE,其中show、select等查询类的属于READ类型,而update、delete等则属于WRITE类型。
- 在WRITE类型中,会将ID IN (:list) 这种语句自动翻译成Batch语句来处理。但是在转换成Batch来处理时,在传入的list中只有一个值时,又会根据参数个数,将Batch转化成了SingleBatch,因此造成了这种不统一的情况(两者的返回值不一样,一个是Integer,一个是int[])
@Override
public Object execute(SQLType sqlType, StatementRuntime... runtimes) {
switch (runtimes.length) {
case 0:
return 0;
case 1:
return executeSingle(runtimes[0], returnType);
default:
return executeBatch(runtimes);
}
}
- 因此对照着源码又看了一下新版本,发现已经对executeBatch做了修正,增加了对一般数据库SQL返回影响行数的支持(有趣的是,也并没有放弃之前对int[]的支持,虽然可以理解为向下兼容,但是个人觉得其实也是想保持这种有趣的对数据处理的理解吧,挺有意思的)
- 1.2.2的版本
private Object executeBatch(StatementRuntime... runtimes) {
int[] updatedArray = new int[runtimes.length];
Map<String, List<StatementRuntime>> batchs = new HashMap<String, List<StatementRuntime>>();
for (int i = 0; i < runtimes.length; i++) {
StatementRuntime runtime = runtimes[i];
List<StatementRuntime> batch = batchs.get(runtime.getSQL());
if (batch == null) {
batch = new LinkedList<StatementRuntime>();
batchs.put(runtime.getSQL(), batch);
}
runtime.setProperty("_index_at_batch_", i); // 该runtime在batch中的位置
batch.add(runtime);
}
// TODO: 多个真正的batch可以考虑并行执行~待定
for (Map.Entry<String, List<StatementRuntime>> batch : batchs.entrySet()) {
String sql = batch.getKey();
List<StatementRuntime> batchRuntimes = batch.getValue();
StatementRuntime runtime = batchRuntimes.get(0);
DataAccess dataAccess = dataAccessProvider.getDataAccess(//
runtime.getMetaData(), runtime.getProperties());
List<Object[]> argsList = new ArrayList<Object[]>(batchRuntimes.size());
for (StatementRuntime batchRuntime : batchRuntimes) {
argsList.add(batchRuntime.getArgs());
}
int[] batchResult = dataAccess.batchUpdate(sql, argsList);
if (batchs.size() == 1) {
updatedArray = batchResult;
} else {
int index_at_sub_batch = 0;
for (StatementRuntime batchRuntime : batchRuntimes) {
Integer _index_at_batch_ = batchRuntime.getProperty("_index_at_batch_");
updatedArray[_index_at_batch_] = batchResult[index_at_sub_batch++];
}
}
}
return updatedArray;
}
- 2.0u8的版本
private Object executeBatch(StatementRuntime... runtimes) {
int[] updatedArray = new int[runtimes.length];
Map<String, List<StatementRuntime>> batchs = new HashMap<String, List<StatementRuntime>>();
for (int i = 0; i < runtimes.length; i++) {
StatementRuntime runtime = runtimes[i];
List<StatementRuntime> batch = batchs.get(runtime.getSQL());
if (batch == null) {
batch = new ArrayList<StatementRuntime>(runtimes.length);
batchs.put(runtime.getSQL(), batch);
}
runtime.setAttribute("_index_at_batch_", i); // 该runtime在batch中的位置
batch.add(runtime);
}
// TODO: 多个真正的batch可以考虑并行执行(而非顺序执行)~待定
for (Map.Entry<String, List<StatementRuntime>> batch : batchs.entrySet()) {
String sql = batch.getKey();
List<StatementRuntime> batchRuntimes = batch.getValue();
StatementRuntime runtime = batchRuntimes.get(0);
DataAccess dataAccess = dataAccessFactory.getDataAccess(//
runtime.getMetaData(), runtime.getAttributes());
List<Object[]> argsList = new ArrayList<Object[]>(batchRuntimes.size());
for (StatementRuntime batchRuntime : batchRuntimes) {
argsList.add(batchRuntime.getArgs());
}
int[] batchResult = dataAccess.batchUpdate(sql, argsList);
if (batchs.size() == 1) {
updatedArray = batchResult;
} else {
int index_at_sub_batch = 0;
for (StatementRuntime batchRuntime : batchRuntimes) {
Integer _index_at_batch_ = batchRuntime.getAttribute("_index_at_batch_");
updatedArray[_index_at_batch_] = batchResult[index_at_sub_batch++];
}
}
}
if (returnType == void.class) {
return null;
}
if (returnType == int[].class) {
return updatedArray;
}
if (returnType == Integer.class || returnType == Boolean.class) {
int updated = 0;
for (int value : updatedArray) {
updated += value;
}
return returnType == Boolean.class ? updated > 0 : updated;
}
throw new InvalidDataAccessApiUsageException(
"bad return type for batch update: " + runtimes[0].getMetaData().getMethod());
}
小结
虽然可以理解为是框架本身的一个问题,但是发现对本身框架使用中的一些细节了解的不够深刻,已经对一些不太常见但很有用的信息熟悉不足,导致了还是花费了一些时间在这个问题上。
但总体来说还是蛮有意思的一个问题,一是记录下来给自己做一个回顾,二是如果能帮助到遇到同样问题的同学就太好了~