跳到主要内容

3.6 批量操作

DB.batch 用于批量插入、批量更新和原生 JDBC 批执行。它与“一个 SQL 通过 IN 条件影响多行”是两种不同方式。

实体批量插入

List<User> users = Arrays.asList(
new User("张三", 25),
new User("李四", 30)
);

BatchResult result = DB.batch.insert(users);
BatchResult chunked = DB.batch.insert(users, 500);

第二个参数是每批条数,必须为有效正数。数据量较大时应结合数据库驱动、SQL 大小、连接池和事务日志选择批次大小。

批量插入使用固定列集合:Pojo 路径写入 Entity 的全部映射字段(数据库自增主键除外),Table 路径写入表元数据中的全部列;其中的 null 也会作为 JDBC NULL 绑定,不沿用单条 Insert Wrapper 默认忽略空值的行为。

启用逻辑删除后,批量插入与单条插入还有一个重要差异:只要目标表存在逻辑删除列,且 Pojo 路径能在 Entity 中找到对应字段,框架就会在绑定参数前把每条数据的逻辑删除值直接改为 0。调用方原来传入的 null1 或其他值都会被覆盖,传入的 Entity 或 JSONMap 本身也会被修改。需要导入已删除历史数据时,应改用显式 JDBC SQL 并自行决定该字段的值。

实体批量更新

// 批量更新要求完整行数据;这里只修改查询得到的完整 Entity
List<User> users = DB.pojo.selectWrapper(User.class)
.in(User::getId, Arrays.asList(1001L, 1002L))
.queryBeanList();

for (User user : users) {
user.setStatus(1);
}

BatchResult result = DB.batch.update(users, 500);

实体必须具有框架能够识别的主键,并且每个对象的主键值都不能为 null

DB.batch.update(List<T>)全字段更新:框架为 Entity 的全部映射字段(主键除外)生成固定的 SET 列表,字段值为 null 时会把数据库列更新为 NULLPojoUpdate.ignore(...) 以及单条更新的忽略空值规则不参与这条批量路径。它只适合完整行快照;只更新少数字段时,应使用 UPDATE Wrapper 或显式 JDBC SQL。

按表名批量插入

下面假设 user 表只有 id/name/age/status/deleted 这些可写列,并允许应用分配主键:

List<JSONMap> insertRows = Arrays.asList(
new JSONMap()
.set("id", 1001L)
.set("name", "张三")
.set("age", 25)
.set("status", 0),
new JSONMap()
.set("id", 1002L)
.set("name", "李四")
.set("age", 30)
.set("status", 0)
);

BatchResult inserted = DB.batch.insert("user", insertRows, 500);

Table 批量插入会按表元数据写入全部列,Map 中缺失的普通列会按 NULL 绑定;逻辑删除列则按前述规则强制写为 0

按表名批量更新

List<JSONMap> updateRows = Arrays.asList(
new JSONMap()
.set("id", 1001L) // 数据库中真实存在的主键
.set("name", "张三")
.set("age", 26)
.set("status", 1)
.set("deleted", 0),
new JSONMap()
.set("id", 1002L) // 数据库中真实存在的主键
.set("name", "李四")
.set("age", 31)
.set("status", 1)
.set("deleted", 0)
);

BatchResult updated = DB.batch.update("user", updateRows, 500);

按表名批量更新时,每行都必须包含表元数据识别出的真实主键值。框架会更新主键之外的全部表列:普通列缺失或值为 null 时会写入 NULL;逻辑删除列缺失或值为 null 时,当前实现会先把该值改为 0

批量更新与逻辑删除

Pojo 和 Table 批量更新都直接执行 UPDATE ... WHERE 主键 = ?,不会自动追加 deleted = 0 之类的未删除条件,因此能够命中已经逻辑删除的数据。Pojo 路径会在 Entity 映射了逻辑删除字段时按其当前值写回;Table 路径在该值缺失或为 null 时会写入 0。如果数据库中的记录已经是 deleted = 1,而批量数据携带或被补成 0,这条记录会被“复活”。

业务不允许更新已删除数据时,应使用带明确逻辑删除条件的 UPDATE Wrapper,或用 DB.batch.execute(...) 编写 WHERE id = ? AND deleted = 0 的批量 JDBC SQL;不要只依赖批量 API 自动过滤。

原生 JDBC 批执行

String sql = "UPDATE user SET status = ? WHERE id = ?";
List<Object[]> params = Arrays.asList(
new Object[]{1, 1001L},
new Object[]{1, 1002L}
);

BatchResult result = DB.batch.execute(sql, params, 500);

值使用 JDBC 参数绑定。表名、列名和 SQL 结构不能来自未经校验的用户输入。

检查执行结果

BatchResult result = DB.batch.insert(users, 500);
if (!result.isSuccess()) {
log.warn("status={}, failed={}, cause={}",
result.status(),
result.failedPositions(),
result.cause());
}

BatchResult 提供:

  • isSuccess()status():总体状态;
  • totalItems()batchSize()batchCount():任务规模;
  • completedBatches():已完成批次;
  • knownAffectedRows()unknownAffectedRows():可确认和不可确认的影响行数;
  • failedPositions():失败数据位置;
  • cause():失败原因。

驱动可能返回“成功但影响行数未知”,因此不能只根据影响行数判断整个批次是否成功。

通过条件影响多行

一次 SQL 更新多条记录时使用 Wrapper:

int affected = DB.pojo.updateWrapper(User.class)
.set(User::getStatus, 0)
.in(User::getId, Arrays.asList(1L, 2L, 3L))
.execute();

批量删除使用 deleteByIdsin(...) 条件:

int affected = DB.pojo.deleteByIds(User.class, ids);

DB.pojo.deleteWrapper(User.class)
.in(User::getId, ids)
.execute();

当前没有 DB.batch.delete(...)

事务与性能

  • 批量调用不天然表示全有或全无;需要整体回滚时放入 DB.tx.run(...) 或框架事务;
  • 避免循环执行大量单条 INSERT/UPDATE;
  • 不要盲目把全部数据放进一个巨大批次;
  • 生产批次大小应通过真实数据库和驱动测试;
  • 失败后重试前,应先判断已经完成的批次和业务幂等性。