语雀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} // 有变更,回显正确
错误结论
从上边的探究过程似乎可以得出以下结论,但是这个结论是错误的
只有待写入数据与数据库中的数据有差异时,主键才能正常回显
我们继续向下分析
深究
那我现在就有几个问题了:
- 如果待记录与数据库完全相同,那究竟有没有执行写数据库操作呢?
- 为什么有时候回显?有时候不回显?
- 正常回显是怎么实现回显的?
如果待记录与数据库完全相同,那究竟有没有执行写数据库操作呢?
我们先将数据库记录值设置一下

现在我们可以插入一条完全相同的数据,看下影响的行数就能够知道是否有更新了
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
}
