关于Db.batchSave()和Db.batchUpdate()的问题和解决

最近我发现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:ListUtils和MapUtils是来自阿里EasyExcel中的工具类

PS:我项目中实际使用了Jboot,然而Jboot关闭了JFinal自带的sql打印,转而使用Jboot实现的,但是对于以上2种方法,却没有任何sql语句打印,让我一开始遇到bug时一脸懵逼。。。因为这个bug不会报错,但是数据库数据就是不对


评论区

快乐的蹦豆子

2023-02-20 12:28

model不是一样的吗

北流家园网

2023-02-20 19:32

我发也现这个问题了,我以为是要有相同数量的字段才能保存成功

星矢

2023-02-25 17:09

不如在数据的源头就杜绝缺少字段的问题。 当set('xx', null) 是, 属性是存在的,不会丢失

大个

2023-02-27 15:40

@星矢 有时候我希望某个字段不设置,自动取数据库默认值,无法set('xx', xxx)因为你不知道数据库默认值,或者可以知道(MetaData),但是要提高代码复杂度

JFinal

2023-11-30 18:50

备忘一下这个改进的原理:按照modifyFlag不同分组,分批执行

这确实是个不错的方案,或许是最好的方案。

如果数据量大的话,性能怎么样?

热门分享

扫码入社