对于upsert操作计算特殊一点
- 如果执行UPDATE会当作2来计算,如果执行INSERT会当作1来算,传给mybatis的返回值也是这样
- 存在MySQL版本差异,但是都是延续旧版本的计算逻辑
- 可能会根据JDBC的连接参数发生变化,比如设置 jdbc:mysql://localhost:3306/db?useAffectedRows=true ,执行UPDATE操作时只会返回1(表示"实际更新的行数")
在 MySQL 中执行 UPSERT(INSERT ... ON DUPLICATE KEY UPDATE) 操作时,如果出现 更新 1 条数据但返回值(affectedRows)为 2 的情况,这通常与 MySQL 的 affectedRows 计算逻辑有关。以下是详细解释:
1. UPSERT 的 affectedRows 计算规则
MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 语句的返回值(affectedRows)遵循以下规则:
- 如果插入成功(无主键/唯一键冲突) :
affectedRows = 1(表示插入了 1 条新记录)。
- 如果更新成功(发生主键/唯一键冲突) :
- MySQL 5.7 及以下版本 :
affectedRows = 2(表示"1 条记录被影响,但更新了 2 行")。- 这里的
2是 1(原始行) + 1(更新后的行),但实际只更新了 1 条记录。
- MySQL 8.0+ :
- 默认情况下,
affectedRows = 2(与 5.7 相同)。 - 但如果启用
CLIENT_FOUND_ROWS选项(JDBC 驱动可能配置),则返回1(表示"找到了 1 行")。
- 默认情况下,
- MySQL 5.7 及以下版本 :
2. 为什么更新 1 条数据却返回 2?
(1) MySQL 的 affectedRows 计算方式
-
对于
ON DUPLICATE KEY UPDATE,MySQL 的affectedRows计算逻辑是:- 如果发生唯一键冲突 :
- 先删除旧行(
affectedRows += 1)。 - 再插入新行(
affectedRows += 1)。 - 因此,即使只更新了 1 条记录,返回值也可能是
2。
- 先删除旧行(
- 如果发生唯一键冲突 :
-
示例 :
sql`INSERT INTO table (id, name) VALUES (1, 'A') ON DUPLICATE KEY UPDATE name = 'B';`- 如果
id=1已存在:- MySQL 5.7 及以下:返回
2(1 删除 + 1 插入)。 - MySQL 8.0+:默认返回
2,但某些驱动可能返回1。
- MySQL 5.7 及以下:返回
- 如果
(2) JDBC 驱动的影响
- 不同版本的 MySQL JDBC 驱动(如
mysql-connector-java)可能对affectedRows的处理方式不同:- 默认情况下,驱动可能返回 MySQL 原生的
2。 - 如果启用了
useAffectedRows=false(默认值),则返回2。 - 如果启用
useAffectedRows=true,则返回1(表示"实际更新的行数")。
- 默认情况下,驱动可能返回 MySQL 原生的
3. 如何验证?
(1) 直接在 MySQL 命令行测试
sql
`CREATE TABLE test (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 第一次插入(affectedRows = 1)
INSERT INTO test (id, name) VALUES (1, 'A');
-- 第二次插入(触发 UPSERT,affectedRows = 2)
INSERT INTO test (id, name) VALUES (1, 'B') ON DUPLICATE KEY UPDATE name = 'B';`
- 在 MySQL 5.7 及以下,第二次执行返回
2。 - 在 MySQL 8.0+,默认返回
2,但某些配置可能返回1。
(2) 检查 JDBC 驱动配置
-
在 MyBatis 的 JDBC URL 中,检查是否有
useAffectedRows参数:properties`jdbc:mysql://localhost:3306/db?useAffectedRows=true`useAffectedRows=true→ 返回1(实际更新的行数)。useAffectedRows=false(默认) → 返回2(MySQL 原生逻辑)。
4. 如何解决?
(1) 接受 MySQL 的默认行为
- 如果业务不依赖
affectedRows的精确值,可以忽略此差异。
(2) 修改 JDBC 驱动配置
-
在连接 URL 中添加
useAffectedRows=true:properties`jdbc:mysql://localhost:3306/db?useAffectedRows=true`- 这样,
ON DUPLICATE KEY UPDATE返回1(表示"实际更新的行数")。
- 这样,
(3) 使用 ROW_COUNT() 函数
-
在 SQL 中显式计算影响的行数:
sql`INSERT INTO test (id, name) VALUES (1, 'A') ON DUPLICATE KEY UPDATE name = 'B'; SELECT ROW_COUNT(); -- 返回 1(MySQL 8.0+)或 2(5.7 及以下)`
(4) 升级 MySQL 或 JDBC 驱动
- MySQL 8.0+ 的某些驱动版本可能优化了
affectedRows的计算方式。
5. 总结
| 场景 | MySQL 5.7 及以下 | MySQL 8.0+ | useAffectedRows=true |
|---|---|---|---|
INSERT ON DUPLICATE KEY UPDATE |
返回 2 |
默认返回 2 |
返回 1 |
| 业务影响 | 可能误导 | 可能优化 | 更符合预期 |
关键结论
ON DUPLICATE KEY UPDATE返回2是 MySQL 的默认行为,表示"1 行被删除 + 1 行被插入"。- JDBC 驱动配置(
useAffectedRows)可以改变返回值 ,使其更符合直觉(返回1)。 - 如果业务依赖
affectedRows的精确值 ,建议显式配置useAffectedRows=true或调整逻辑。
6. 最佳实践
-
在 MyBatis 中配置 JDBC URL :
properties`jdbc:mysql://localhost:3306/db?useAffectedRows=true` -
在代码中处理可能的差异 :
java`int affectedRows = sqlSession.update("upsertData", params); if (affectedRows == 1 || affectedRows == 2) { // 视为成功(1=更新,2=MySQL 默认行为) }` -
使用 MySQL 8.0+ 并检查驱动版本,确保行为一致。
这样,即使执行 UPSERT 时返回 2,也能正确处理业务逻辑。
ON DUPLICATE KEY UPDATE 的实际执行逻辑并非先删除后插入,而是直接更新已存在的记录。以下是详细分析:
1. 执行逻辑解析
- 无冲突时 :
- 执行标准
INSERT操作,插入新行,affectedRows = 1。
- 执行标准
- 存在冲突时 (主键或唯一键重复):
-
直接更新指定字段,而非删除旧记录再插入新记录。
-
例如:
sql`INSERT INTO table (id, name) VALUES (1, 'A') ON DUPLICATE KEY UPDATE name = 'B';`- 如果
id=1已存在,MySQL 会直接更新name字段为'B',不会删除旧记录。
- 如果
-
2. 与 REPLACE INTO 的区别
REPLACE INTO:- 先删除旧记录(触发
DELETE操作),再插入新记录。 - 可能导致自增主键不连续,且可能触发外键约束或触发器。
- 先删除旧记录(触发
ON DUPLICATE KEY UPDATE:- 直接更新字段,不删除旧记录,因此不会影响自增主键或触发额外操作。
3. 为什么返回值可能是 2?
- MySQL 5.7 及以下版本 :
- 返回
2是历史遗留行为,表示"1 行被影响(更新)"。 - 这里的
2是 1(原始行) + 1(更新后的行) 的逻辑计算,但实际只更新了 1 条记录。
- 返回
- MySQL 8.0+ :
- 默认仍返回
2,但可通过 JDBC 驱动配置(useAffectedRows=true)改为返回1(表示实际更新的行数)。
- 默认仍返回
4. 关键优势
- 数据完整性 :
- 不会因删除操作触发外键约束或触发器。
- 性能优化 :
- 避免不必要的删除和插入操作,减少锁竞争。
- 自增主键友好 :
- 不会因更新操作导致自增 ID 不连续。
5. 示例验证
sql
`-- 创建测试表
CREATE TABLE test (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 插入初始数据
INSERT INTO test (id, name) VALUES (1, 'A');
-- 执行 UPSERT
INSERT INTO test (id, name) VALUES (1, 'B')
ON DUPLICATE KEY UPDATE name = 'B';
-- 查询结果
SELECT * FROM test; -- 输出: id=1, name='B'(旧记录未被删除)`
- 结果证明:
ON DUPLICATE KEY UPDATE直接更新了字段,未删除旧记录。
6. 总结
| 特性 | ON DUPLICATE KEY UPDATE |
REPLACE INTO |
|---|---|---|
| 执行逻辑 | 直接更新字段 | 先删除后插入 |
| 数据完整性 | 保留旧记录 | 可能破坏完整性 |
| 自增主键影响 | 无影响 | 导致 ID 不连续 |
| 返回值(MySQL 5.7) | 可能为 2 |
可能为 2 |
| 返回值(MySQL 8.0+) | 默认 2(可配置为 1) |
默认 2 |
结论 :ON DUPLICATE KEY UPDATE 的核心逻辑是 直接更新已存在的记录 ,而非先删除后插入。其返回值 2 是 MySQL 的历史行为,可通过配置优化。该特性在数据完整性、性能和自增主键管理上优于 REPLACE INTO。