最近我发现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