使用Gorm进行批量更新--clause子句的妙用

9,979 阅读5分钟

基本概念

我们在讨论更新的时候需要明白一点:更新是针对数据库列的操作。使用gorm操作数据库,我们通常会建一个与表结构对应的模型结构体,该模型的每一个字段都对应了数据库表的一列。gorm提供了很多关于更新的操作,比如针对单个字段的db.Update(),针对多个字段的db.Updates()具体内容不再赘述,大家自己查阅官方文档

问题引出

翻阅文档我们可以发现,常用的更新操作都是对特定字段用同一个值进行更新,例如对于student表的score字段进行更新调用db.Update()方法时只会把score列更新为同一个值。如果在某些业务场景下,需要把每一行数据的某个字段更新为不同的值,这个时候最容易想到的办法就是用for循环遍历每一行数据,针对每一行进行不同的更新操作。这种方法虽然能够完成需求,但每执行一次更新都会发起一次对数据库的io操作,这样如果数据量大的话会很慢,有没有一次io就能完成批量更新的方法呢?

解决方案

gorm在创建数据的时候可以用db.Create()进行批量创建,只需要用一个切片存放需要创建的数据然后create该切片即可,具体的可以参考官方文档。在此方法中只对数据库进行了一次io操作,我们可以基于该方法进行一次io的批量更新操作。

clause语句的OnConflict{}构造器

gorm中的clause语句提供了,对sql子句的构建操作。对于每个操作,GORM 都会创建一个 *gorm.Statement 对象,所有的 GORM API 都是在为 statement 添加、修改 子句,最后,GORM 会根据这些子句生成 SQL。下面介绍一下网上引用较多的gorm提供的clause.OnConflict{}子句构造器。

type OnConflict struct {
	Columns      []Column
	Where        Where
	TargetWhere  Where
	OnConstraint string
	DoNothing    bool
	DoUpdates    Set
	UpdateAll    bool
}

OnConflict{}是gorm提供的一个用于处理冲突的构造器,当在插入数据遇到冲突的时候,比如数据已经存在,我们可以通过OnConflict{}指定一些操作,例如存在则更新。下面介绍一些其中的某些字段

  • Columns: 定义重复键冲突时需要检查的列。
  • Where: 定义检查重复键冲突时的条件。
  • OnConstraint: 定义检查重复键冲突时使用的约束。
  • DoNothing: 定义当重复键冲突时不执行任何操作。
  • DoUpdates: 定义当重复键冲突时执行更新操作,需要传入一个map[string]interface{}类型的参数,表示需要更新的列及其对应的值。
  • UpdateAll: 定义当重复键冲突时更新所有列的值,需要传入一个结构体的指针,表示需要更新的记录。

通过OnConflict的定义我们可以看出,当遇到冲突需要更新的时候我们需要给Columns填入需要检查冲突的列,给DoUpdates传入要执行的操作即可。不过,怎么给DoUpdates赋值呢?

AssignmentColumns函数

clause.AssignmentColumns()用于在发生冲突时指定要更新的列,我们需要传递一个string类型的切片,在切片中指定要更新的字段名。假如有一个user表当检测到id发生冲突的时候更新nameage列,我们就可以这样操作

db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)

使用案例

我们用一个案例来给本文进行最后的收尾。假如有一个student表,表的定义和数据库初始化等操作如下:

//表结构定义
type Student struct {
	Name  string
	Score int
}

func main() {
	dsn := "root:123456@tcp(localhost:3306)/chen?charset=utf8&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Println("数据库连接失败:", err)
		return
	}
	db.AutoMigrate(&Student{})
	var data = []Student{
		{ID: 1, Name: "张三", Score: 100},
		{ID: 2, Name: "李四", Score: 100},
		{ID: 3, Name: "王五", Score: 100},
		{ID: 4, Name: "赵六", Score: 100},
	}
	err = db.Create(&data).Error
	if err != nil {
		fmt.Println("插入数据出错:", err)
		return
	}
	//查询数据是否插入成功
	var res []Student
	if err = db.Find(&res).Error; err != nil {
		fmt.Println("查询数据出错:", err)
		return
	}
	print(res)
}
//用来打印切片数据的打印函数
func print(datas []Student) {
	for _, data := range datas {
		fmt.Printf("%+v\n", data)
	}
}

控制台输出结果:

chen@DESKTOP-RA4F79H MINGW64 /d/GoProject/src/GormClauseDemo
$ go run main.go
{ID:1 Name:张三 Score:100}
{ID:2 Name:李四 Score:100}
{ID:3 Name:王五 Score:100}
{ID:4 Name:赵六 Score:100}

现在我们修改每个学生的分数都不同的值

//修改切片内的每个分数
	for k, _ := range res {
		res[k].Score = k
	}
	//查看修改后的数据
	print(res)
//控制台输出
chen@DESKTOP-RA4F79H MINGW64 /d/GoProject/src/GormClauseDemo
$ go run main.go
{ID:1 Name:张三 Score:0}
{ID:2 Name:李四 Score:1}
{ID:3 Name:王五 Score:2}
{ID:4 Name:赵六 Score:3}

这里可以看到切片里的每个学生分数都被修改成了不同的值,

这里有个小细节:我用for range修改切片数据的时候为啥不用直接修改k,v参数的第二个v,而是要通过res[k]的方式修改。因为for range在遍历的时候是把切片复制了一份直接修改value不会影响原有切片,而通过res[k]的方式修改的是切片的底层数组,就算是复制的切片底层数据指向的也是同一个数组,所以用这种方式可以修改原有切片。

执行批量修改:

这里将重要代码放一起

fmt.Println("修改前数据库的数据:")
	var res []Student
	if err = db.Find(&res).Error; err != nil {
		fmt.Println("查询数据出错:", err)
		return
	}
	print(res)
	//修改切片内的每个分数
	for k, _ := range res {
		res[k].Score = k
	}
	//查看修改后的数据
	fmt.Println("期望修改后的数据:")
	print(res)
	//批量修改
	fmt.Println("修改后数据库的数据:")
	db.Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "id"}},
		DoUpdates: clause.AssignmentColumns([]string{"score"}),
	}).Create(&res)
	//查询修改后的数据
	if err = db.Find(&res).Error; err != nil {
		fmt.Println("查询数据出错:", err)
		return
	}
	print(res)
chen@DESKTOP-RA4F79H MINGW64 /d/GoProject/src/GormClauseDemo
$ go run main.go
修改前数据库的数据:
{ID:1 Name:张三 Score:100}
{ID:2 Name:李四 Score:100}
{ID:3 Name:王五 Score:100}
{ID:4 Name:赵六 Score:100}
期望修改后的数据:
{ID:1 Name:张三 Score:0}
{ID:2 Name:李四 Score:1}
{ID:3 Name:王五 Score:2}
{ID:4 Name:赵六 Score:3}
修改后数据库的数据:
{ID:1 Name:张三 Score:0}
{ID:2 Name:李四 Score:1}
{ID:3 Name:王五 Score:2}
{ID:4 Name:赵六 Score:3}

可以看到数据的确被修改了,至此我们所期望的不用for循环进行批量修改的需求完成了。希望本篇文章对大家有所帮助,如果大家有不同的见解欢迎在评论区留言讨论!