Go 学习笔记 - gorm + mysql 使用小知识及避坑指南

本文最后更新于:2023年8月8日 晚上

描述

作为一个 gorm 的重度使用者,在习惯了 gorm 带来的便利,也遇到了很多的问题,日前看到一些小伙伴仍然会因为对 gorm 特性的不了解,导致出现 panic 或者数据的错误更新的问题,所以这边提供一些小技巧和避坑指南,持续更新…

插入

gorm 解决并发插入报错

当你有并发插入的情况,就难免会出现数据冲突的问题,如果不做处理的话,会出现报错 Duplicate key,gorm 提供了中间件来解决这个问题。

如果你期望 upsert:

1
2
doc := &model.Table{Title: "table"}
err := db.Table("table").Clauses(clause.OnConflict{UpdateAll: true}).Create(&doc).Error

你也可以根据实际使用场景选择其他不同的冲突处理方案:

1
2
3
4
5
6
7
8
9
type OnConflict struct {
Columns []Column
Where Where
TargetWhere Where
OnConstraint string
DoNothing bool
DoUpdates Set
UpdateAll bool
}

Clauses on conflict do nothing 带来的问题

如果你和我一样习惯用结构体指针插入,那么如果用 Clauses OnConflict do nothing 解决冲突的同时可能会带来另外一个问题,那就是数据不冲突时,insert id 会回写,但是冲突之后不会进行回写。

部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
if !db.DryRun && db.Error == nil {
result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)

if err == nil {
db.RowsAffected, _ = result.RowsAffected()

if db.RowsAffected > 0 {
if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil && db.Statement.Schema.PrioritizedPrimaryField.HasDefaultValue {
if insertID, err := result.LastInsertId(); err == nil && insertID > 0 {
switch db.Statement.ReflectValue.Kind() {
case reflect.Slice, reflect.Array:
if config.LastInsertIDReversed {
for i := db.Statement.ReflectValue.Len() - 1; i >= 0; i-- {
rv := db.Statement.ReflectValue.Index(i)
if reflect.Indirect(rv).Kind() != reflect.Struct {
break
}

_, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(rv)
if isZero {
db.Statement.Schema.PrioritizedPrimaryField.Set(rv, insertID)
insertID -= db.Statement.Schema.PrioritizedPrimaryField.AutoIncrementIncrement
}
}
} else {
for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
rv := db.Statement.ReflectValue.Index(i)
if reflect.Indirect(rv).Kind() != reflect.Struct {
break
}

if _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(rv); isZero {
db.Statement.Schema.PrioritizedPrimaryField.Set(rv, insertID)
insertID += db.Statement.Schema.PrioritizedPrimaryField.AutoIncrementIncrement
}
}
}
case reflect.Struct:
if _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.ReflectValue); isZero {
db.Statement.Schema.PrioritizedPrimaryField.Set(db.Statement.ReflectValue, insertID)
}
}
} else {
db.AddError(err)
}
}
}
} else {
db.AddError(err)
}
}

大概意思就是,如果影响行数 > 1 才会进行 insert id 赋值,也就是说,如果此时直接使用 doc 中的 id,那么这个 id 的值会是 0。

批量 upsert id 错误

1
2
doc := []*model.Table{{Title: "table",Content:"xx"},{Title:"table1",Content:"yy"}}
err := db.Table("table").Clauses(clause.OnConflict{UpdateAll: true}).Create(&doc).Error

Title 为唯一主键,如果数据库里已经存在了 title 为 table 的数据,那么 content 可以成功更新,响应行数也 >1 ,但是 id 的回写会从 lastInsertID 向下自增,也就是说,两个结构体回写的 id 都是错误的,这种情况直接使用,风险很大,可以考虑判断影响行数是否符合预期,不符合预期重新查询来解决这个问题。


Go 学习笔记 - gorm + mysql 使用小知识及避坑指南
https://agopher.com/2023/08/08/tech/2023_go_gorm/
作者
冷宇生(Allen)
发布于
2023年8月8日
更新于
2023年8月8日
许可协议