一、问题背景:一个看似成功的插入,却返回了"魔数"
今天,我在调试设备注册功能时,发现如下代码:
java
Device device = new Device();
device.setMac("AA:BB:CC:DD:EE:FF");
device.setDeviceid("dev_12345");
device.setCreateUserId(1001);
int result = deviceMapper.addDevice(device);
log.info("插入结果: {}", result); // 输出:-2147482646
然后立刻去数据库查询:
sql
SELECT * FROM device WHERE mac = 'AA:BB:CC:DD:EE:FF';
-- ✅ 数据存在!
现象总结:
- ✅ SQL 执行成功,数据写入数据库;
- ❌ 方法返回值为 -2147482646,而非预期的 1;
- ❌ 即使添加 @Options(useGeneratedKeys = false),问题依旧;
- ❌ 无异常抛出,无事务回滚,无唯一键冲突。
这让人陷入困惑:MyBatis 到底发生了什么?
二、问题定位:配置中的"隐形杀手"
2.1 排查过程回顾
| 排查方向 | 结果 |
|---|---|
| 是否 SQL 错误? | ❌ SQL 正确,数据已写入 |
| 是否唯一索引冲突? | ❌ MAC 和 deviceId 均为新值 |
| 是否主键回填失败? | ❌ 添加 useGeneratedKeys=false 无效 |
| 是否 JDBC 驱动 bug? | ❌ 升级至 mysql-connector-j:9.0.0 仍存在 |
| 是否 MyBatis 执行器配置问题? | ✅ 命中! |
2.2 真正的罪魁祸首:mybatis.configuration.default-executor-type: batch
在 application.yml 中,你为了支持批量更新,配置了:
yml
mybatis:
configuration:
default-executor-type: batch # ← 全局开启批处理
这个配置让 所有 Mapper 方法默认使用 BatchExecutor。
三、深度原理:为什么 BATCH 模式会返回 -2147482646?
3.1 MyBatis 执行器(Executor)机制
MyBatis 通过 Executor 接口抽象 SQL 执行策略,主要有三种实现:
| 执行器 | 类名 | 执行器 | 类名 | 行为 | 返回值 |
|---|---|---|---|---|---|
| SIMPLE | SimpleExecutor |
SIMPLE | SimpleExecutor |
每次调用 executeUpdate() 立即执行 |
正常行数(1, 0) |
| REUSE | ReuseExecutor |
REUSE | ReuseExecutor |
缓存 PreparedStatement 重用 |
正常行数 |
| BATCH | BatchExecutor |
BATCH | BatchExecutor |
缓存 SQL,延迟批量执行 | NO_UPDATE_COUNT = -2147482646 |
📌 源码位置:
org.apache.ibatis.executor.BatchExecutor
javapublic static final int NO_UPDATE_COUNT = Integer.MIN_VALUE + 1001; // = -2147482646
3.2 BATCH 模式的工作流程
调用mapper.insert(...)→ MyBatis 将 SQL 加入内部 batch 队列;
- 不立即执行,也不获取影响行数;
- 返回预定义常量
NO_UPDATE_COUNT; - 直到
SqlSession.commit()或flushStatements()时,才真正批量执行。
因此:
- 返回值无业务意义,不能用于判断单条是否成功;
- 数据最终会写入(如果事务提交);
- 但开发者误以为"方法失败"。
四、为什么会开启 batch 模式?
通常是为了支持类似这样的批量更新需求:
java
int updateCreateUserName(@Param("list") List<UpdateUserNameMQ> list);
XML 写法:
xml
<update id="updateCreateUserName">
<foreach collection="list" item="item">
UPDATE device SET create_user_name = #{item.userName}
WHERE create_user_id = #{item.userId};
</foreach>
</update>
我之前以为 <foreach> = 批量高效,但实际上:
- 它生成 N 条独立的
UPDATE语句; - 在
SIMPLE模式下:N 次网络往返,性能差; - 在
BATCH模式下:N 条 SQL 被批量发送,性能提升。 - 于是我开启了全局
batch,却牺牲了所有单条操作的语义完整性。
五、正确解决方案(含代码+原理)
✅ 方案一:关闭全局 batch + 改用单 SQL 批量更新(强烈推荐)
步骤 1:关闭全局配置
yml
mybatis:
configuration:
# default-executor-type: batch ← 删除此行
步骤 2:改写 XML 为 CASE WHEN 单 SQL
xml
<update id="updateCreateUserName">
UPDATE device
SET create_user_name =
CASE
<foreach collection="list" item="item">
WHEN create_user_id = #{item.userId} THEN #{item.userName}
</foreach>
END
WHERE create_user_id IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.userId}
</foreach>
</update>
✅ 优势分析:
| 维度 | 说明 |
|---|---|
| 性能 | 1 条 SQL,1 次网络往返,数据库执行计划更优 |
| 返回值 | 正常返回影响行数(如 3) |
| 兼容性 | 不依赖 MyBatis 执行器类型,适用于任何环境 |
| 可读性 | SQL 逻辑清晰,易于 DBA 审核 |
| 事务安全 | 原子性保证(整条 SQL 成功或失败) |
✅ 方案二:使用 INSERT ... ON DUPLICATE KEY UPDATE(需唯一索引)
我的 create_user_id 有唯一约束(或主键),可使用 MySQL 特有语法:
xml
<update id="updateCreateUserName">
INSERT INTO device (create_user_id, create_user_name)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.userName})
</foreach>
ON DUPLICATE KEY UPDATE
create_user_name = VALUES(create_user_name)
</update>
✅ 优势:
- 性能极佳(一条语句完成插入/更新);
- 天然支持大批量(万级数据);
❌ 前提:
- 必须有唯一索引;
- 表结构允许插入(非空字段需有默认值)。
✅ 方案三:Service 层手动控制 Batch(高级用法)
适用于后台任务、数据迁移等场景。
- Mapper 提供单条更新方法
java
int updateSingleCreateUserName(@Param("item") UpdateUserNameMQ item);
xml
<update id="updateSingleCreateUserName">
UPDATE device SET create_user_name = #{item.userName}
WHERE create_user_id = #{item.userId}
</update>
- Service 层手动开启 BATCH
java
@Service
public class DeviceBatchService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void batchUpdateUserNames(List<UpdateUserNameMQ> list) {
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
DeviceMapper mapper = session.getMapper(DeviceMapper.class);
for (UpdateUserNameMQ item : list) {
mapper.updateSingleCreateUserName(item);
}
session.commit(); // 触发批量执行
} // 自动 flush + close
}
}
✅ 优势:
- 真正的 JDBC Batch(addBatch() + executeBatch());
- 内存友好(流式处理);
- 可控性强。
⚠️ 注意:
- 该方法不返回有效行数,需通过其他方式验证;
- 不适用于 Web 请求高频调用(SqlSession 生命周期需谨慎管理)。
六、常见误区澄清
| 误区 | 事实 |
|---|---|
"加 @Options(useGeneratedKeys=false) 能解决" |
❌ 与主键回填无关 |
"@Options(executorType = BATCH) 可以指定执行器" |
❌ 注解无此属性 |
"<foreach> 就是批量更新" |
❌ 只是多条 SQL 拼接 |
| "返回 -2147482646 表示 SQL 失败" | ❌ 表示"计数不可用" |
七、最佳实践总结
- 不要开启全局
default-executor-type: batch
→ 它会破坏单条 DML 的语义,导致返回值不可信。 - 优先使用单 SQL 批量更新
→CASE WHEN或ON DUPLICATE KEY UPDATE是更优雅的解决方案。 - 如需极致性能,使用 Service 层手动 BATCH
→ 适用于离线任务,避免污染在线接口。 - 永远不要依赖
BATCH模式的返回值做业务判断
→ 它的设计初衷就是"不返回行数"。 - 定期审查 MyBatis 配置
→ 避免"为了 A 功能,牺牲 B 功能"的配置陷阱。
八、附录:完整配置示例
application.yml(推荐配置)
yaml
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
# 不设置 default-executor-type,默认为 SIMPLE
mapper-locations: classpath:mapper/*.xml
DeviceMapper.java
java
@Mapper
public interface DeviceMapper {
int addDevice(Device device); // 返回 1
int updateCreateUserName(@Param("list") List<UpdateUserNameMQ> list); // 返回实际影响行数
}
device-mapper.xml
xml
<insert id="addDevice">
INSERT INTO device(mac, deviceId, create_user_id)
VALUES (#{mac}, #{deviceId}, #{createUserId})
</insert>
<update id="updateCreateUserName">
UPDATE device
SET create_user_name = CASE
<foreach collection="list" item="item">
WHEN create_user_id = #{item.userId} THEN #{item.userName}
</foreach>
END
WHERE create_user_id IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.userId}
</foreach>
</update>
九:结语
一个小小的配置参数 default-executor-type: batch,竟引发了如此复杂的连锁反应。这提醒我们:
技术选型和配置,必须理解其底层原理和副作用。
"能用"不等于"好用","简单"不等于"正确"。
希望本文能帮助你避开这个坑,并掌握 MyBatis 批量操作的正确姿势
如有疑问或补充,欢迎留言讨论!