惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
博客园 - 聂微东
I
InfoQ
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
Y
Y Combinator Blog
F
Full Disclosure
量子位
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
GbyAI
GbyAI
S
SegmentFault 最新的问题
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
月光博客
月光博客
人人都是产品经理
人人都是产品经理
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
B
Blog RSS Feed
The Last Watchdog
The Last Watchdog
Cisco Talos Blog
Cisco Talos Blog
T
Threat Research - Cisco Blogs
Stack Overflow Blog
Stack Overflow Blog
J
Java Code Geeks
S
Securelist
宝玉的分享
宝玉的分享
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Secure Thoughts
F
Fortinet All Blogs
T
Tor Project blog
B
Blog
L
LangChain Blog
博客园 - 司徒正美
Spread Privacy
Spread Privacy
The Cloudflare Blog
A
Arctic Wolf
W
WeLiveSecurity
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
T
Troy Hunt's Blog
Help Net Security
Help Net Security
博客园_首页
Hugging Face - Blog
Hugging Face - Blog
Project Zero
Project Zero

二丫讲梵

学习周刊-总第258期-2026年第15周 学习周刊-总第257期-2026年第14周 学习周刊-总第256期-2026年第13周 学习周刊-总第255期-2026年第12周 学习周刊-总第254期-2026年第11周 学习周刊-总第253期-2026年第10周 临时插播一条羊毛,免费领取450元大模型API代金券 学习周刊-总第252期-2026年第09周 学习周刊-总第251期-2026年第08周 学习周刊-总第250期-2026年第07周 学习周刊-总第249期-2026年第06周 诚邀评论,聊聊你所知欲知的我 学习周刊-总第248期-2026年第05周 我的QQ动态之2015年 我的QQ动态之2014年 我的QQ动态之2013年 我的QQ动态之2012年 我的QQ动态-2010-2011年 我的QQ动态-创栏小叙 学习周刊-总第247期-2026年第04周 Nexus社区版权益阉割--一文告诉你有哪些版本可以选择 学习周刊-总第246期-2026年第03周 学习周刊-总第245期-2026年第02周 学习周刊-总第244期-2026年第01周 学习周刊-总第243期-2025年第52周 学习周刊-总第242期-2025年第51周 开源项目ZenOps:带你领略禅意运维 学习周刊-总第241期-2025年第50周 用京东金融,享负债人生 学习周刊-总第240期-2025年第49周 学习周刊-总第239期-2025年第48周 整理我在静态服务透明代理上的极致求索之路,最后一个你绝想不到 学习周刊-总第238期-2025年第47周 我们输了,但收获颇多 太多了,太多了 学习周刊-总第237期-2025年第46周 学习周刊-总第236期-2025年第45周 学习周刊-总第235期-2025年第44周 二五年国庆二三事 学习周刊-总第234期-2025年第43周 学习周刊-总第233期-2025年第42周 学习周刊-总第232期-2025年第41周 学习周刊-总第231期-2025年第40周 学习周刊-总第230期-2025年第39周 西湖毅行 2025年开源世界逸闻三则 学习周刊-总第229期-2025年第38周 手抄《与妻书》 CNB开发与构建基于docker-cache缓存复用的配置实践心得 学习周刊-总第228期-2025年第37周 父亲善行录 学习周刊-总第227期-2025年第36周 带你认识我之看看我的高中同学录(其二) 学习周刊-总第226期-2025年第35周 带你认识我之看看我的高中同学录(其一) 认识神级MCP工具系列--用anyquery和数据库交互 学习周刊-总第225期-2025年第34周 学习周刊-总第224期-2025年第33周 学习周刊-总第223期-2025年第32周 学习周刊-总第222期-2025年第31周 学习周刊-总第221期-2025年第30周 CNB云原生开发环境届的瑞士军刀,详解qifei项目 vuepress-vdoing主题配置自建不蒜子统计 学习周刊-总第220期-2025年第29周 写在博客发表文章1000篇的节点 从claude cli的体验聊聊最大的敌人是我们自己的成见 学习周刊-总第219期-2025年第28周 学习周刊-总第218期-2025年第27周 学习周刊-总第217期-2025年第26周 理论正确,事实错误 学习周刊-总第216期-2025年第25周 学习周刊-总第215期-2025年第24周 学习周刊-总第214期-2025年第23周 学习周刊-总第213期-2025年第22周 学习周刊-总第212期-2025年第21周 从赵心童世锦赛夺冠聊聊我的斯诺克情缘 学习周刊-总第211期-2025年第20周 记录二五年五一之短暂回归家庭 学习周刊-总第210期-2025年第19周 学习周刊-总第209期-2025年第18周 学习周刊-总第208期-2025年第17周 Go开发实践之Gin框架将前端的dist目录embed到二进制 学习周刊-总第207期-2025年第16周 近期关于cobra库的一些实践心得总结 学习周刊-总第206期-2025年第15周 我的嗜好--嗑瓜子 记大宝参加幼儿园组织的清明烈士陵园扫墓活动 学习周刊-总第205期-2025年第14周 学习周刊-总第204期-2025年第13周 前端开发小笔记--在数组中给字段添加值与删除字段的操作 学习周刊-总第203期-2025年第12周 人生实苦,何以自渡 学习周刊-总第202期-2025年第11周 近期发现的一些优秀的工具提名(一) 学习周刊-总第201期-2025年第10周 学习周刊-总第200期-2025年第09周 记一次女友喝醉使我忍术破功 爱情风波--一次分手又复合的经历 学习周刊-总第199期-2025年第08周 整理我的信息源
利用gorm自身提供的方法实现存在更新不存在则创建的能力
二丫讲梵 · 2023-12-31 · via 二丫讲梵

# 背景

在一些定时任务将数据同步到 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
	}
}

1
2
3
4
5
6
7
8
9
10

MySQL 有一个语句是 UPSERT 的操作,它结合了 updateinsert 两种操作的功能。当执行 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`)

1

# 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
}

1
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
}

1
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
}

1
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
}

1
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
}

1
2
3
4
5
6

表示当 user_name 和 sex 这两个字段都唯一的时候更新,不唯一的时候则新增。 注意: 多个字段时,需要先创建一个联合唯一索引:

CREATE UNIQUE INDEX idx_name ON user (user_name, sex);

1

# 实践演示

整体 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")
	}
}

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

此时执行如上代码,首次执行会发现 user 表将会写入三条数据,然后你可以手动更改其中一条数据的 nick_name,接着再次执行如上代码,可以看到字段又会更新为如上示例数据。