最近我发现Db.batchSave()和Db.batchUpdate()的源码中都有如下这一段:
//=====================batchUpdate=========================
Model model = modelList.get(0);
// 新增支持 modifyFlag
if (model.modifyFlag == null || model.modifyFlag.isEmpty()) {
return new int[0];
}
Set<String> modifyFlag = model._getModifyFlag();
//=====================batchSave=========================
Model model = modelList.get(0);
Map<String, Object> attrs = model._getAttrs();也就是说,无论批量更新还是插入,都只会根据List中第一个元素的属性设置情况来决定向数据库插入或更新哪些字段,如此肯定是有问题的。
Bug重现:
假定有如下表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`code` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
对应的实体类略。。。
User u1=new User();//不设置任何属性
User u2=new User();
u2.setName("aa");
u2.setCode("bb");
List<User> users = new ArrayList<>();
users.add(u1);
users.add(u2);
Db.batchSave(users,100);运行结果:数据库中2条记录name和code字段都是null
以下工具类是我自己写的,可以解决这个问题:
public class DbKit {
/**
* 批量操作数据库时数量
*/
public static final int DB_BATCH_COUNT = 100;
/**
* 原有框架方法更新只会取modelList第一个元素的字段状态,批量更新的SQL全部相同,只是参数值不同
* 本方法会根据modelList中所有元素,生成不同的SQL和参数,分批分别执行
* 自动过滤所有null值属性
* @param modelList
* @param batchSize
* @return
*/
public static List<Integer> batchListUpdate(List<? extends Model> modelList, int batchSize){
if (modelList == null || modelList.size() == 0)
return ListUtils.newArrayList();
Map<String, ModelBatchInfo> modelUpdateMap= MapUtils.newHashMap();
for (Model model : modelList) {
Set<String> modifyFlag = CPI.getModifyFlag(model);
Config config = CPI.getConfig(model);
Table table = TableMapping.me().getTable(model.getClass());
String[] pKeys = table.getPrimaryKey();
Map<String, Object> attrs = CPI.getAttrs(model);
List<String> attrNames = new ArrayList<>();
// the same as the iterator in Dialect.forModelSave() to ensure the order of the attrs
for (Map.Entry<String, Object> e : attrs.entrySet()) {
String attr = e.getKey();
if (modifyFlag.contains(attr) && !config.getDialect().isPrimaryKey(attr, pKeys) && table.hasColumnLabel(attr))
attrNames.add(attr);
}
for (String pKey : pKeys)
attrNames.add(pKey);
String columns = StrKit.join(attrNames.toArray(new String[attrNames.size()]), ",");
ModelBatchInfo updateInfo= modelUpdateMap.get(columns);
if(updateInfo==null){
updateInfo=new ModelBatchInfo();
updateInfo.modelList=ListUtils.newArrayList();
StringBuilder sql = new StringBuilder();
config.getDialect().forModelUpdate(TableMapping.me().getTable(model.getClass()), attrs, modifyFlag, sql, new ArrayList<>());
updateInfo.sql=sql.toString();
modelUpdateMap.put(columns,updateInfo);
}
updateInfo.modelList.add(model);
}
return batchModelList(modelList, batchSize, modelUpdateMap);
}
private static List<Integer> batchModelList(List<? extends Model> modelList, int batchSize, Map<String, ModelBatchInfo> modelUpdateMap) {
List<Integer> ret = ListUtils.newArrayListWithExpectedSize(modelList.size());
//批量更新
for (Map.Entry<String, ModelBatchInfo> entry : modelUpdateMap.entrySet()) {
int[] batch = Db.batch(entry.getValue().sql, entry.getKey(), entry.getValue().modelList, batchSize);
for (int i : batch) {
ret.add(i);
}
}
return ret;
}
/**
* 原有框架方法更新只会取modelList第一个元素的字段状态,批量插入的SQL全部相同,只是参数值不同
* 本方法会根据modelList中所有元素,生成不同的SQL和参数,分批分别执行
* 自动过滤所有null值属性
* @param modelList
* @param batchSize
* @return
*/
public static List<Integer> batchListSave(List<? extends Model> modelList, int batchSize){
if (modelList == null || modelList.size() == 0)
return ListUtils.newArrayList();
Map<String, ModelBatchInfo> modelUpdateMap= MapUtils.newHashMap();
for (Model model : modelList) {
Config config = CPI.getConfig(model);
Map<String, Object> attrs = CPI.getAttrs(model);
int index = 0;
StringBuilder columns = new StringBuilder();
// the same as the iterator in Dialect.forModelSave() to ensure the order of the attrs
for (Map.Entry<String, Object> e : attrs.entrySet()) {
if (config.getDialect().isOracle()) { // 支持 oracle 自增主键
Object value = e.getValue();
if (value instanceof String && ((String)value).endsWith(".nextval")) {
continue ;
}
}
if (index++ > 0) {
columns.append(',');
}
columns.append(e.getKey());
}
String cs = columns.toString();
ModelBatchInfo batchInfo= modelUpdateMap.get(cs);
if(batchInfo==null){
batchInfo=new ModelBatchInfo();
batchInfo.modelList=ListUtils.newArrayList();
StringBuilder sql = new StringBuilder();
config.getDialect().forModelSave(TableMapping.me().getTable(model.getClass()), attrs, sql, new ArrayList());
batchInfo.sql=sql.toString();
modelUpdateMap.put(cs,batchInfo);
}
batchInfo.modelList.add(model);
}
return batchModelList(modelList, batchSize, modelUpdateMap);
}
public static class ModelBatchInfo {
public String sql;
public List modelList;
}
}PS:我项目中实际使用了Jboot,然而Jboot关闭了JFinal自带的sql打印,转而使用Jboot实现的,但是对于以上2种方法,却没有任何sql语句打印,让我一开始遇到bug时一脸懵逼。。。因为这个bug不会报错,但是数据库数据就是不对
项目:JFinal