利用gorm自身提供的方法实现存在更新不存在则创建的能力
# 背景
在一些定时任务将数据同步到 MySQL 的场景,我们会有一种需求是:如果该条数据存在则更新,不存在则创建。
如果使用常规的思路,比如遍历 100 条数据,则需要先通过 find 判断是否存在,存在则更新,不存在则创建,如此下来,将会与数据库有 200 次查询与写入(或更新)的交互,当数据量大了之后,这种交互就会给数据库带来不小的开销。
这种思路大概的伪代码如下:
for _, user := range users {
results := db.Table(tableName).Where("id = ?", user.ID).First(&user)
if results.Error != nil {
if results.Error == gorm.ErrRecordNotFound {
_ := db.Table(tableName).Create(&user)
}
} else {
_ = db.Table(tableName).Where("id = ?", user.ID).Updates(user).Error
}
}
2
3
4
5
6
7
8
9
10
MySQL 有一个语句是 UPSERT 的操作,它结合了 update
和 insert
两种操作的功能。当执行 upsert 操作时,如果指定的记录已经存在,则执行更新操作;如果指定的记录不存在,则执行插入操作。这种操作可以用来确保数据的一致性,并且可以减少对数据库的访问次数。
其中判断记录是否存在的依据是表中的唯一键或主键。
这种操作的语句大概如下:
INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`user_name`,`nick_name`) VALUES ('2023-12-31 04:07:36.502','2023-12-31 04:07:36.502',NULL,'eryajf1','二丫讲梵1') ON DUPLICATE KEY UPDATE `updated_at`='2023-12-31 04:07:36.502',`deleted_at`=VALUES(`deleted_at`),`user_name`=VALUES(`user_name`),`nick_name`=VALUES(`nick_name`)
# gorm 的实现
在 gorm 当中,作者也提供了对应的方法,让我们能够直接使用这种方法来实现这种能力。官方文档地址 (opens new window)。
# 标准实现
示例代码如下:
type User struct {
gorm.Model
UserName string `gorm:"size:10;column:user_name" json:"userName"` // 用户名
NickName string `gorm:"size:24;column:nick_name" json:"nickName"` // 昵称
}
func UpSert(users User) error {
return db.Debug().Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(&users).Error
}
2
3
4
5
6
7
8
9
10
11
12
此处代码意思是使用 id(在 gorm 中,id 字段默认 tag 为
primarykey
,即主键) 作为判断依据,如果对应 ID 的用户已存在,则进行更新,如果不存在,则创建。注意:
不要在 gorm 的 tag 中定义default:NULL;
这样的参数,否则更新的功能可能会失效。
# 指定判断字段
但大多数时候,定时任务拿到的原始数据中还没有 MySQL 库里的 ID,所以我们不太会用 id 来作为判断依据,这里假设利用 user_name
来作为唯一值来进行判断。
示例代码如下:
type User struct {
gorm.Model
UserName string `gorm:"size:10;column:user_name;uniqueIndex" json:"userName"` // 用户名
NickName string `gorm:"size:24;column:nick_name" json:"nickName"` // 昵称
}
func UpSert(users User) error {
return db.Debug().Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_name"}},
UpdateAll: true,
}).Create(&users).Error
}
2
3
4
5
6
7
8
9
10
11
12
在这个示例中,在 gorm 的 tag 中,你需要将
user_name
定义为uniqueIndex
,即唯一索引。然后 UpSert 的字段指定该字段即可。
# 批量处理
如上示例是针对单条记录的处理,该方法还支持对一组数据的处理,示例代码如下:
type User struct {
gorm.Model
UserName string `gorm:"size:10;column:user_name;uniqueIndex" json:"userName"` // 用户名
NickName string `gorm:"size:24;column:nick_name" json:"nickName"` // 昵称
}
func UpSerts(users []User) error {
return db.Debug().Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_name"}},
UpdateAll: true,
}).Create(&users).Error
}
2
3
4
5
6
7
8
9
10
11
12
# 其他补充
# 指定更新字段
如上示例当中,都是使用的 UpdateAll: true
的参数,如果在的应用场景中,并不希望所有的字段都更新,而是更新指定字段,则可以使用如下方式进行更新字段的定义:
func UpSerts(users []User) error {
return db.Debug().Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_name"}},
DoUpdates: clause.AssignmentColumns([]string{"nick_name"}),
}).Create(&users).Error
}
2
3
4
5
6
通过
DoUpdates
指定只更新 nick_name 字段的值,其余字段则不更新。
# 定义多个判断依据
从字段 Columns
的类型可以看到,此处可指定多个字段,写法如下:
func UpSerts(users []User) error {
return db.Debug().Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_name"},{Name: "sex"}},
DoUpdates: clause.AssignmentColumns([]string{"nick_name"}),
}).Create(&users).Error
}
2
3
4
5
6
表示当 user_name 和 sex 这两个字段都唯一的时候更新,不唯一的时候则新增。
注意:
多个字段时,需要先创建一个联合唯一索引:
CREATE UNIQUE INDEX idx_name ON user (user_name, sex);
# 实践演示
整体 demo 演示代码如下:
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var db *gorm.DB
// InitDB 初始化DB
func InitDB() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&collation=%s&%s",
"root",
"123456",
"localhost",
3306,
"test-gorm",
"utf8mb4",
"utf8mb4_general_ci",
"parseTime=true",
)
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
// 禁用外键(指定外键时不会在mysql创建真实的外键约束)
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
panic(fmt.Errorf("初始化mysql数据库异常: %v", err))
}
// 2, 把模型与数据库中的表对应起来
db.AutoMigrate(
&User{},
)
}
// User 用户模型
type User struct {
gorm.Model
UserName string `gorm:"size:10;column:user_name;uniqueIndex" json:"userName"` // 用户名
NickName string `gorm:"size:24;column:nick_name" json:"nickName"` // 昵称
}
func UpSerts(users []User) error {
return db.Debug().Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_name"}},
UpdateAll: true,
}).Create(&users).Error
}
func main() {
// 1,初始化
InitDB()
var us []User
us = append(us, User{UserName: "eryajf1", NickName: "二丫讲梵1"},
User{UserName: "eryajf2", NickName: "二丫讲梵2"},
User{UserName: "eryajf3", NickName: "二丫讲梵3"})
err := UpSerts(us)
if err != nil {
fmt.Printf("upsert err : %v\n", err)
} else {
fmt.Println("success")
}
}
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
此时执行如上代码,首次执行会发现 user 表将会写入三条数据,然后你可以手动更改其中一条数据的 nick_name
,接着再次执行如上代码,可以看到字段又会更新为如上示例数据。