【踩坑】gorm 回写主键不正确

语雀UI更舒服:www.yuque.com/anthonyzhao...

前置知识

GORM 官方文档中说,当通过数据的指针来创建数据时,会回写插入数据的主键。即便是批量插入,GORM 也会返回所有主键值。 GORM 创建

相关验证

但是文档中并没有说插入冲突则执行更新操作会不会回写主键,我们先验证一下。

数据库中现有记录情况如下

执行批量插入更新操作

go 复制代码
func main() {
	db := NewDB()
	testGorm1 := &TestGorm{
		UK: "uk1",
	}
	testGorm2 := &TestGorm{
		UK: "uk2",
	}
	err := db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches([]*TestGorm{testGorm1, testGorm2}, 1).Error
	if err != nil {
		panic("failed to create test_gorm")
	}
	fmt.Printf("%+v\n", testGorm1)
	fmt.Printf("%+v\n", testGorm2)
}
go 复制代码
&{ID:1 UK:uk1 CreatedAt:1763774066 UpdatedAt:1763774066}
&{ID:2 UK:uk1 CreatedAt:1763774066 UpdatedAt:1763774066}

通过输出发现,批量插入更新也是可以正常回显主键的。

至此,都是符合预期的。

主键回显异常情况

当批量传入的数据有相同唯一键时,只有第一个对象能够正常回写主键,后续主键均回写错误。

go 复制代码
func main() {
	db := NewDB()
	testGorm1 := &TestGorm{UK: "uk1", UpdatedAt: 1}
	testGorm2 := &TestGorm{UK: "uk1", UpdatedAt: 1}
	testGorm3 := &TestGorm{UK: "uk1", UpdatedAt: 1}
	err := db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches([]*TestGorm{testGorm1, testGorm2, testGorm3}, 1).Error
	if err != nil {
		panic("failed to create test_gorm")
	}
	fmt.Println(testGorm1)
	fmt.Println(testGorm2)
	fmt.Println(testGorm3)
}
go 复制代码
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:1}
&{ID:0 UK:uk1 CreatedAt:0 UpdatedAt:1}
&{ID:0 UK:uk1 CreatedAt:0 UpdatedAt:1}

探索

【猜测】:后边的ID都不对,有没有可能是数据库中现在这条记录的ID就是0了呢?还抱着一丝回显正确的希望,查一下看看

完蛋🙄,咋可能主键会更新呢。

【猜测】:那改下更新的记录值呢?

go 复制代码
func main() {
	db := NewDB()
	testGorm1 := &TestGorm{UK: "uk1", UpdatedAt: 2}
	testGorm2 := &TestGorm{UK: "uk1", UpdatedAt: 2}
	testGorm3 := &TestGorm{UK: "uk1", UpdatedAt: 3}
	err := db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches([]*TestGorm{testGorm1, testGorm2, testGorm3}, 1).Error
	if err != nil {
		panic("failed to create test_gorm")
	}
	fmt.Printf("%+v\n", testGorm1)
	fmt.Printf("%+v\n", testGorm2)
	fmt.Printf("%+v\n", testGorm3)
}
go 复制代码
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:2}
&{ID:0 UK:uk1 CreatedAt:0 UpdatedAt:2}
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:3}

握🌿,第1,2条咋主键又不对了呢?2,3 可以条咋又可以正常回显了呢。

【猜测】:难道是GORM发现待保存的数据存在完全相同的,框架给去重了?

那我就只更新一条

go 复制代码
func main() {
	db := NewDB()
	// testGorm1 := &TestGorm{UK: "uk1", UpdatedAt: 1}
	// testGorm2 := &TestGorm{UK: "uk1", UpdatedAt: 2}
	testGorm3 := &TestGorm{UK: "uk1", UpdatedAt: 3}
	err := db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches([]*TestGorm{testGorm3}, 1).Error
	if err != nil {
		panic("failed to create test_gorm")
	}
	// fmt.Printf("%+v\n", testGorm1)
	// fmt.Printf("%+v\n", testGorm2)
	fmt.Printf("%+v\n", testGorm3)
}
go 复制代码
&{ID:0 UK:uk1 CreatedAt:0 UpdatedAt:3}

又一次我🌿,更新一条也不能回显了

猛然发现,这条数据与数据库中的记录值完全一样诶🫤。

【猜测】是不是没执行更新操作,然后就没回显呢?

go 复制代码
func main() {
	db := NewDB()
	testGorm1 := &TestGorm{UK: "uk1", UpdatedAt: 1} // 有变更
	testGorm2 := &TestGorm{UK: "uk1", UpdatedAt: 2} // 有变更
	testGorm3 := &TestGorm{UK: "uk1", UpdatedAt: 2}
	testGorm4 := &TestGorm{UK: "uk1", UpdatedAt: 3} // 有变更
	testGorm5 := &TestGorm{UK: "uk1", UpdatedAt: 3}
	testGorm6 := &TestGorm{UK: "uk1", UpdatedAt: 4} // 有变更
	testGorm7 := &TestGorm{UK: "uk1", UpdatedAt: 5} // 有变更
	testGorm8 := &TestGorm{UK: "uk1", UpdatedAt: 6} // 有变更
	testGorm9 := &TestGorm{UK: "uk1", UpdatedAt: 7} // 有变更

	err := db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches([]*TestGorm{
			testGorm1, testGorm2, testGorm3, 
			testGorm4, testGorm5, testGorm6, 
			testGorm7, testGorm8, testGorm9,
		}, 1).Error
	if err != nil {
		panic("failed to create test_gorm")
	}
	fmt.Printf("%+v\n", testGorm1)
	fmt.Printf("%+v\n", testGorm2)
	fmt.Printf("%+v\n", testGorm3)
	fmt.Printf("%+v\n", testGorm4)
	fmt.Printf("%+v\n", testGorm5)
	fmt.Printf("%+v\n", testGorm6)
	fmt.Printf("%+v\n", testGorm7)
	fmt.Printf("%+v\n", testGorm8)
	fmt.Printf("%+v\n", testGorm9)
}
go 复制代码
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:1} // 有变更,回显正确
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:2} // 有变更,回显正确
&{ID:0 UK:uk1 CreatedAt:0 UpdatedAt:2} // 【未变更,回显错误】
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:3} // 有变更,回显正确
&{ID:0 UK:uk1 CreatedAt:0 UpdatedAt:3} // 【未变更,回显错误】
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:4} // 有变更,回显正确
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:5} // 有变更,回显正确
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:6} // 有变更,回显正确
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:7} // 有变更,回显正确

错误结论

从上边的探究过程似乎可以得出以下结论,但是这个结论是错误的

只有待写入数据与数据库中的数据有差异时,主键才能正常回显

我们继续向下分析

深究

那我现在就有几个问题了:

  1. 如果待记录与数据库完全相同,那究竟有没有执行写数据库操作呢?
  2. 为什么有时候回显?有时候不回显?
  3. 正常回显是怎么实现回显的?

如果待记录与数据库完全相同,那究竟有没有执行写数据库操作呢?

我们先将数据库记录值设置一下

现在我们可以插入一条完全相同的数据,看下影响的行数就能够知道是否有更新了

go 复制代码
func main() {
	db := NewDB()
	testGorm1 := &TestGorm{UK: "uk1", UpdatedAt: 1} 
	
	res := db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches([]*TestGorm{testGorm1,}, 1)
	fmt.Printf("影响的行数: %+v\n", res.RowsAffected)
}
go 复制代码
影响的行数: 0

所以结论是: 如果待记录与数据库完全相同,数据库不会执行更新操作

顺手看了一下,更新操作会影响的行数,惊呆了,老铁😦

go 复制代码
影响的行数: 2

各种查找原因,在MYSQL官方文档中找了一些线索

With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values.

在使用ON DUPLICATE KEY UPDATE(重复键更新)时,每行对应的 "受影响行数"(affected-rows)值遵循以下规则:

  • 若该行作为新行被插入,该值为 1;
  • 若已有行被更新,该值为 2;
  • 若已有行被设置为其当前值(即未发生实际数据变更),该值为 0。

为什么有时候回显主键,有时候不回显主键?

答案当然是在源码中了 callbacks/create.go:123

正常回显是怎么实现回显的?

那就要看下源码设置回显主键的具体逻辑, callbacks/create.go:200

又一次无语了,那岂不是只有第一条是对的,其他的都是错的了.........

还记得上边的错误结论么?上述实验的batchsize都是1,按照源码来看,同一批次内,只有第一个记录的主键回显是正确的,其余记录哪怕是与数据库中的记录有差异,也不会正确回显。因为,其余记录是在GORM在内存中自己根据数据库自增步长**私自**设置的。验证下

go 复制代码
func main() {
	db := NewDB()
	testGorms := make([]*TestGorm, 0)
	for i := 1; i <= 10; i++ {
		testGorms = append(testGorms, &TestGorm{
			UK:        "uk1",
			UpdatedAt: i,
		})
	}

	_ = db.Clauses(clause.OnConflict{UpdateAll: true}).
		CreateInBatches(testGorms, 5)
	for _, t := range testGorms {
		fmt.Printf("%+v\n", t)
	}
}
go 复制代码
&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:1}
&{ID:2 UK:uk1 CreatedAt:0 UpdatedAt:2}
&{ID:3 UK:uk1 CreatedAt:0 UpdatedAt:3}
&{ID:4 UK:uk1 CreatedAt:0 UpdatedAt:4}
&{ID:5 UK:uk1 CreatedAt:0 UpdatedAt:5}

&{ID:1 UK:uk1 CreatedAt:0 UpdatedAt:6}
&{ID:2 UK:uk1 CreatedAt:0 UpdatedAt:7}
&{ID:3 UK:uk1 CreatedAt:0 UpdatedAt:8}
&{ID:4 UK:uk1 CreatedAt:0 UpdatedAt:9}
&{ID:5 UK:uk1 CreatedAt:0 UpdatedAt:10}

10个记录分两批,每个记录都不同,的确是只有每个批次第一个记录主键是正确的,其余的都是自增的。

正确结论

汇总一下正确结论

如果待记录与数据库完全相同,数据库不会执行更新操作

在使用ON DUPLICATE KEY UPDATE(重复键更新)时,每行对应的 "受影响行数"(affected-rows)值遵循以下规则:

  • 若该行作为新行被插入,该值为 1;
  • 若已有行被更新,该值为 2;
  • 若已有行被设置为其当前值(即未发生实际数据变更),该值为 0。

GORM 主键正确回显条件太苛刻,不建议使用(几乎不可用)

如何才能回显主键

需要批量插入更新且需要回显主键的场景,肯定是有一个业务唯一键的。

那么最容易想到的方式就是,插入后,然后再根据业务唯一键批量查出。

GORM还支持另外一种方式,Hook。可以保存后查出,然后手动设置ID。不严谨地测试了一下,小数量情况下(2W左右)二者性能相差不是很大。主要是,Hook 实现更优雅一些。

go 复制代码
func (t *TestGorm) AfterSave(tx *gorm.DB) error {
	var id int
	tx = tx.Table(t.TableName()).Where("uk = ?", t.UK).
		Select("id").Scan(&id)
	if tx.Error != nil {
		return tx.Error
	}
	t.ID = uint(id)
	return nil
}
相关推荐
q_19132846952 小时前
基于SpringBoot2+Vue2的宠物健康医疗论坛系统
vue.js·spring boot·mysql·健康医疗·宠物·计算机毕业设计
q***75182 小时前
Linux(CentOS)安装 MySQL
linux·mysql·centos
G***E3162 小时前
MySQL增强现实案例
数据库·mysql·ar
小胖同学~2 小时前
浅浅的聊聊MySQL的MVCC
mysql
w***74403 小时前
SQL Server 数据库迁移到 MySQL 的完整指南
android·数据库·mysql
凌寒1111 小时前
Linux(Debian)安装、卸载 MySQL
linux·运维·mysql·debian
oneslide12 小时前
分享一个MySQL数据库备份恢复脚本--II
数据库·mysql
q***239213 小时前
MySQL数据库误删恢复_mysql 数据 误删
数据库·mysql·adb
合作小小程序员小小店13 小时前
web网页开发,在线%图书管理%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·后端·mysql·jdk·intellij-idea