【踩坑】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
}
相关推荐
万亿少女的梦1689 小时前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
万亿少女的梦1689 小时前
基于SpringBoot与Vue的历史博物馆展品展示与游客管理系统设计
spring boot·mysql·vue·系统设计·博物馆管理系统
灯澜忆梦12 小时前
MySQL 完整学习路线
学习·mysql
ClouGence12 小时前
保障公共安全,一线治安机构实时数据应用与实践
数据库·mysql·postgresql
Minxinbb12 小时前
TDSQL for MySQL 服务器重启操作
数据库·mysql·dba
渣渣灰飞13 小时前
MySQL 系统学习 第二阶段 第三章 DQL(Data Query Language)第三节:ORDER BY + LIMIT(排序 + 分页)
数据库·学习·mysql
fengbingchun16 小时前
客户端获取服务器端MySQL数据库表中数据C++实现
mysql
渣渣灰飞16 小时前
MySQL 系统学习 第二阶段 第三章 DQL(Data Query Language)第二节:WHERE 条件查询
sql·学习·mysql
不知疲倦的仄仄17 小时前
ShardingSphere:高效管理数据库集群的终极方案
sql·mysql·oracle·database
山川而川-R17 小时前
MySql-安装详细教程
数据库·mysql