当前位置: 首页 > news >正文

批量更新操作全攻略:从JDBC原理到多框架实现(MyBatis/MyBatis-Plus/Nutz)

【保姆级总结】批量更新操作全攻略:从JDBC原理到多框架实现(MyBatis/MyBatis-Plus/Nutz)

在后端开发中,批量更新(如批量指派需求、批量修改状态)是高频场景。若用for循环+单条SQL实现,会因频繁网络交互导致性能骤降(10万条数据需10万次数据库请求)。本文从底层原理出发,梳理JDBC批量更新核心逻辑,并给出MyBatis、MyBatis-Plus、Nutz等主流框架的实战方案,附带避坑指南,新手也能直接复用。

1. 批量更新底层核心原理(JDBC层面)

所有框架的批量更新,本质都是对JDBC批量机制的封装,核心目标是「减少Java应用与数据库的网络交互次数」,关键原理如下:

1.1 核心逻辑:合并请求+复用编译

  • 预编译SQL复用:通过PreparedStatement创建SQL模板(如UPDATE table SET col=? WHERE id=?),仅预编译1次,避免重复解析SQL的开销。
  • 批量任务暂存:用addBatch()将多组参数(如不同id和col值)暂存到内存,而非立即发送数据库。
  • 一次请求执行:调用executeBatch()时,将所有暂存任务打包,通过1次网络请求发送给数据库,大幅减少IO开销。

1.2 关键:真批量 vs 伪批量

并非调用批量API就高效,核心看数据库是否支持「真批量」:

  • 伪批量(默认情况):数据库将批量任务拆分为多条独立SQL(如UPDATE...;UPDATE...),仅减少网络交互,数据库仍需逐条执行(效率提升有限)。
  • 真批量(需配置):数据库将任务合并为单条批量SQL(如UPDATE table SET col=CASE id WHEN ? THEN ? END WHERE id IN(?)),仅解析执行1次,效率提升10倍以上。

1.3 数据库关键配置(必加)

要开启「真批量」,需在JDBC连接URL中添加参数,不同数据库配置不同:

数据库

关键参数

说明

MySQL/MariaDB

rewriteBatchedStatements=true

重写批量SQL为单条CASE WHEN语句

PostgreSQL

reWriteBatchedInserts=true

插入场景专用,更新无需额外配置

Oracle

无需配置

天然支持真批量

示例(MySQL/MariaDB URL):

jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true&useSSL=false

1.4 事务配合(避坑重点)

JDBC默认「自动提交事务」(autoCommit=true),若不调整:

  • 每执行1条SQL就提交1次事务(事务提交需写磁盘,开销极大)。
  • 正确做法:批量操作前关闭自动提交,所有批次执行完后统一提交,异常时全量回滚:
// JDBC原生事务控制(框架会自动封装)
connection.setAutoCommit(false);
try {ps.executeBatch(); // 执行批量任务connection.commit(); // 统一提交
} catch (SQLException e) {connection.rollback(); // 异常回滚
} finally {connection.setAutoCommit(true); // 恢复默认
}

2. 各框架批量更新实战实现

以下方案均基于「分批次+真批量+事务控制」设计,适配10万级数据量,直接复制即可用。

2.1 MyBatis:分批次+CASE WHEN(推荐)

MyBatis无内置批量更新方法,需自定义SQL实现「单批CASE WHEN+多批循环」,核心是避免SQL过长(每批1000-2000条)。

步骤1:Mapper接口(单批处理)
public interface DemandMapper {// 单批批量更新(参数为单批数据列表)int batchUpdateDemand(@Param("list") List<Demand> demandList);
}
步骤2:XML映射文件(CASE WHEN拼接)
<update id="batchUpdateDemand">UPDATE v_pm_task_info_listSET assign_user_id = #{list[0].assignUserId}, -- 单批内统一值(不同值需加CASE)assign_username = #{list[0].assignUsername},update_time = NOW(),assign_count = CASE id -- 动态值:按id匹配不同count<foreach collection="list" item="item" separator="">WHEN #{item.id} THEN #{item.assignCount} + 1</foreach>ENDWHERE id IN <foreach collection="list" item="item" open="(" close=")" separator=",">#{item.id}</foreach>
</update>
步骤3:Service层(分批次+事务)
@Service
public class DemandService {@Autowiredprivate DemandMapper demandMapper;private static final int BATCH_SIZE = 1000; // 每批1000条// 事务注解:确保所有批次统一提交/回滚@Transactional(rollbackFor = Exception.class)public void batchAssignDemand(List<Demand> allDemands) {if (CollectionUtils.isEmpty(allDemands)) return;// 分批次循环处理for (int i = 0; i < allDemands.size(); i += BATCH_SIZE) {// 截取当前批次(避免越界)int end = Math.min(i + BATCH_SIZE, allDemands.size());List<Demand> batchList = allDemands.subList(i, end);// 执行单批更新int affectRows = demandMapper.batchUpdateDemand(batchList);// 校验结果:避免部分更新失败if (affectRows != batchList.size()) {throw new RuntimeException("批次[" + i + "-" + end + "]更新失败");}}}
}

2.2 MyBatis-Plus:2种方案适配不同场景

MyBatis-Plus提供简化API,但需区分「中小批量」和「超大量」场景。

方案1:中小批量(updateBatchById)

适合数据量<1万条,底层封装循环但需配置「真批量」参数:

@Service
public class DemandServiceImpl extends ServiceImpl<DemandMapper, Demand> implements DemandService {@Override@Transactional(rollbackFor = Exception.class)public boolean batchAssignSmall(List<Demand> demandList) {// 直接调用MP内置方法,需确保URL配置rewriteBatchedStatements=truereturn updateBatchById(demandList, 1000); // 第2个参数为批次大小}
}
方案2:超大量(自定义SQL+分批次)

同MyBatis的CASE WHEN方案,MP可通过@Update注解省略XML:

public interface DemandMapper extends BaseMapper<Demand> {@Update("<script>" +"UPDATE v_pm_task_info_list " +"SET assign_count = CASE id " +"   <foreach collection='list' item='item' separator=''>" +"       WHEN #{item.id} THEN #{item.assignCount} + 1 " +"   </foreach>" +"END WHERE id IN " +"   <foreach collection='list' item='item' open='(' close=')' separator=','>" +"       #{item.id} " +"   </foreach>" +"</script>")int batchUpdateLarge(@Param("list") List<Demand> demandList);
}

2.3 Nutz:Sql对象+VarSet参数(适配Nutz ORM)

Nutz无内置批量更新API,需通过Sql对象构建批量SQL,核心是用VarSet设置参数(替代JDBC的addBatch())。

实战代码(Service层)
@Service
public class DemandService {@Injectprivate Dao dao;private static final int BATCH_SIZE = 1000;@Tx // Nutz事务注解,等价于Spring的@Transactionalpublic void batchAssignDemand(List<Demand> allDemands) {if (CollectionUtils.isEmpty(allDemands)) return;// 分批次处理for (int i = 0; i < allDemands.size(); i += BATCH_SIZE) {int end = Math.min(i + BATCH_SIZE, allDemands.size());List<Demand> batchList = allDemands.subList(i, end);// 1. 构建批量SQLStringBuilder sql = new StringBuilder();sql.append("UPDATE v_pm_task_info_list SET ");sql.append("assign_user_id = @assignUserId, ");sql.append("assign_count = CASE id ");for (Demand d : batchList) {sql.append("WHEN @id_").append(d.getId()).append(" THEN @count_").append(d.getId()).append(" ");}sql.append("END WHERE id IN (@ids)");// 2. 绑定参数(Nutz用VarSet,避免索引计算)Sql updateSql = Sqls.create(sql.toString());updateSql.vars().set("assignUserId", batchList.get(0).getAssignUserId());List<Long> ids = new ArrayList<>();for (Demand d : batchList) {ids.add(d.getId());updateSql.vars().set("id_" + d.getId(), d.getId());updateSql.vars().set("count_" + d.getId(), d.getAssignCount() + 1);}updateSql.vars().set("ids", ids);// 3. 执行单批更新dao.execute(updateSql);}}
}

2.4 JDBC原生:理解底层流程

若需脱离框架,JDBC原生API是基础,步骤如下:

public void batchUpdateByJdbc(List<Demand> demandList) {String sql = "UPDATE v_pm_task_info_list SET assign_count = ? WHERE id = ?";try (Connection conn = DriverManager.getConnection(URL, USER, PWD);PreparedStatement ps = conn.prepareStatement(sql)) {conn.setAutoCommit(false); // 关闭自动提交int batchIdx = 0;for (Demand d : demandList) {ps.setInt(1, d.getAssignCount() + 1); // 第1个占位符ps.setLong(2, d.getId()); // 第2个占位符ps.addBatch(); // 暂存任务batchIdx++;// 每1000条执行一次批量if (batchIdx % BATCH_SIZE == 0) {ps.executeBatch(); // 执行批量ps.clearBatch(); // 清空任务}}// 执行剩余批次ps.executeBatch();conn.commit(); // 统一提交} catch (SQLException e) {conn.rollback(); // 异常回滚throw new RuntimeException("批量更新失败", e);}
}

3. 高频避坑指南(90%开发者会踩)

坑1:事务未控制,导致部分提交

  • 问题:未加事务注解,某批次失败后,前序批次已提交,数据不一致。
  • 解决:所有批量方法必须加事务(Spring用@Transactional(rollbackFor=Exception.class),Nutz用@Tx)。

坑2:批次过大,导致SQL过长/内存溢出

  • 问题:单批1万条,SQL长度超数据库限制(MySQL默认4M),或内存暂存过多参数OOM。
  • 解决:批次大小设为1000-2000条,根据字段数量调整(字段越多,批次越小)。

坑3:未配置数据库参数,伪批量生效

  • 问题:URL未加rewriteBatchedStatements=true,MySQL拆分为多条SQL,效率无提升。
  • 解决:MySQL/MariaDB必须配置该参数,PostgreSQL/Oracle按需配置。

坑4:GROUP BY/LEFT JOIN导致数据异常

  • 问题:批量更新关联表时,未正确分组或关联条件错误,导致更新行数不符。
  • 解决:GROUP BY需包含所有非聚合字段,LEFT JOIN确保关联字段类型一致(如iteration_idID均为Long)。

4. 最佳实践总结

  1. 框架选择
    • 中小批量(<1万条):MyBatis-Plus updateBatchById(简洁);
    • 超大量(>1万条):MyBatis/Nutz自定义CASE WHEN+分批次(高效);
    • 无框架场景:JDBC原生API(灵活)。
  1. 性能优化点
    • 数据库参数:必加rewriteBatchedStatements=true(MySQL);
    • 批次大小:1000-2000条/批;
    • 参数绑定:用@变量PreparedStatement占位符,避免SQL注入。
  1. 监控与兜底
    • 记录每批执行耗时,便于优化批次大小;
    • 校验executeBatch()返回的影响行数,与批次大小一致,否则回滚。

结语

批量更新的核心是「减少交互+真批量+事务保障」,无论用哪种框架,都需围绕这三点设计。本文代码可直接复制到项目中,只需调整表名、字段名即可适配业务。若有疑问或其他框架需求,欢迎在评论区交流!

http://www.dtcms.com/a/507234.html

相关文章:

  • 简述:普瑞时空数据建库软件(国土变更建库)之一(2025年部分新规则)
  • 零基础新手小白快速了解掌握服务集群与自动化运维(十二)Python编程之面向对象
  • 刚学做网站怎么划算普洱专业企业网站建设
  • Java基础——面向对象复习知识点12
  • IPv6路由技术
  • 网站建设开票开什么内容电脑禁止访问网站设置
  • WPeChatGPT 插件使用教程(转载)
  • 从 Sora 到 Sora 2:文本生成视频进入下一个阶段(附sora教程)
  • k8s(十二)Rancher详解
  • 4. 前馈网络(FeedForward):给每个词“做深度加工”
  • wordpress一步步建企业网站上海有名的广告设计公司
  • 百度搜索站长平台汽车网站建设目的
  • EDA--三井物产商品预测挑战赛 Exploratory Data Analysis(探索性数据分析)
  • 【云计算专题会议】第二届云计算与大数据国际学术会议(ICCBD 2025)
  • AI CRM中的数据分析:悟空AI CRM如何帮助企业优化运营
  • Git多项目提交记录提取与数据分析指南
  • 网站后台账号密码忘记了怎么办漳平网络建站公司
  • 响水做网站价格上海网站设计成功柚v米科技
  • Elasticsearch面试精讲 Day 26:集群部署与配置最佳实践
  • 搭建Jenkins
  • 多语言NLP数据处理:核心环节与实践要点
  • 无法远程连接 MySQL
  • 域名seo站长工具中文网址大全2345
  • 终身免费vps上海搜索优化推广
  • WebDAV 服务搭建指南
  • 射击游戏-辅助瞄准
  • 董付国老师Python小屋编程题答案171-180
  • 15、Python函数-函数传参
  • 手机自助网站建设seo公司发展前景
  • kafka-3.3.1