深入学习gorm系列八:深入理解gorm.Save函数

1,196 阅读6分钟

大家好,我是渔夫子。今天我们学习gorm中的Save操作。大纲如下: image.png

概述

跟gorm.Create函数的行为不同,gorm.Save函数大体上有两个行为:

  • 在待更新的数据不存在的情况下做插入操作
  • 在待更新的数据存在的情况下做更新操作

数据是否存在的一个重要依据就是待更新的记录里是否存在主键字段。

接下来我们就详细了解下在不同的场景下Save函数的行为。

一、表中有主键id字段

首先,我们先建立一个m_test_01表,该表中有id字段作为主键。如下**:**

CREATE TABLE `m_test_01` (
  `id` int not null auto_increment,
  `name` varchar(100) NOT NULL DEFAULT '',
  `userid` int(11) DEFAULT NULL,
  primary key(`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;

我们定义一个对应的Model结构体:

type MTest01 struct {
	Id int
	Name string `gorm:"DEFAULT:John"`
	Userid int `json:"userid"`
}

1.1 待更新数据不包含主键字段

dsn := "user:password@tcp(127.0.0.1:3306)/test01?charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms"

db, _ := gorm.Open(mysql.Open(dsn), nil)

var row = MTest01{
	Name: "Stone",
	Userid: 100,
}

err := db.Save(&row).Error

该代码会转换成以下sql:

INSERT INTO `m_test_01` (`name`,`userid`) VALUES ('Stone',100)

理由:在row中没有对应的主键Id字段,所有只做插入操作这跟gorm.Create行为是一样的。

1.2 待更新的数据包含主键字段

dsn := "user:password@tcp(127.0.0.1:3306)/test01?charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms"

db, _ := gorm.Open(mysql.Open(dsn), nil)

var row = MTest01{
    Id: 1,
	Name: "Stone_Update",
	Userid: 100,
}

err := db.Save(&row).Error

该代码会转换成如下sql:

UPDATE `m_test_01` SET `name`='Stone',`userid`=100 WHERE `id` = 1

这里有个点需要注意:如果该update语句执行成功后,影响的行数大于0,那么该save函数就执行完毕了。如果该语句执行后影响的行数是0,即该行内容没有任何改变,那么还会再执行下如下语句

 INSERT INTO `m_test_01` (`name`,`userid`,`id`) VALUES ('Stone',100,1) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`userid`=VALUES(`userid`)

看到了吧,根据update的最终结果,有可能会和数据库再进行一次交互。

那为什么这里会再执行一次 Insert ... ON DUPLICATE KEY UPDATE 呢? 这个主要是应用于存在唯一索引的情况下,因为主键本身就是一个特殊的唯一索引。所以gorm是尽最大努力来保证数据更新成功。

1.3 待更新的数据只有部分字段

如果待更新的数据中不包含Userid,如下:

dsn := "user:password@tcp(127.0.0.1:3306)/test01?charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms"

db, _ := gorm.Open(mysql.Open(dsn), nil)

var row = MTest01{
    Id: 1,
	Name: "Stone_Update",
}

err := db.Save(&row).Error

那么,最终转换成的sql语句中,会把userid更新成默认值0。如下:

UPDATE `m_test_01` SET `name`='Stone',`userid`=0 WHERE `id` = 1

划重点,待更新的模型数据中只包含表的部分字段时,Save函数会把未指定的字段值更新成对应类型的默认值。

1.4 指定where条件

在Save函数中,Where条件和模型的主键若同时存在,则sql语句的where条件会转换成指定的where条件以及主键。如下:

dsn := "user:password@tcp(127.0.0.1:3306)/test01?charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms"

db, _ := gorm.Open(mysql.Open(dsn), nil)

var row = MTest01{
    Id: 1,
	Name: "Stone_Update",
    Userid: 100,
}

err := db.Where("userid=?", 100).Save(&row).Error

当Save执行的时候会转换成如下sql语句:

UPDATE `m_test_01` SET `name`='Stone',`userid`=100 WHERE userid=100 AND `id` = 1

以上都是基于表有主键字段,而且主键字段名又是id的场景下的行为。那么,如果表的主键字段不是id,而是name,那又如何呢?

二、表的主键字段名非id

我们还是先建立一个表m_test_02,主键字段为name。如下:

CREATE TABLE `m_test_02` (
  `name` varchar(100) NOT NULL DEFAULT '',
  `userid` int(11) DEFAULT NULL,
  primary key(`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建立对应的Model模型,如下:

type MTest02 struct {
	Name string `gorm:"DEFAULT:John"`
	Userid int `json:"userid"`
}

func (m *MTest02) TableName() string {
	return "m_test_02"
}

我们再重新执行如下Save函数:

dsn := "user:password@tcp(127.0.0.1:3306)/test01?charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms"

db, _ := gorm.Open(mysql.Open(dsn), nil)

var row = MTest02{
	Name: "Stone",
	Userid: 100,
}

err := db.Save(&row).Error
fmt.Printf("err is:%+v\n", err)

我们执行该代码时,会发现报错了,说是缺少必要的where条件,如下:

err is:WHERE conditions required

你看,虽然表中有主键字段,单gorm并不认识该字段。那为什么字段id是主键时,gorm就会根据该id进行更新呢?

原因是gorm包在实现时默认优先根据id或ID字段名来进行了一次匹配。相关代码在gorm/schema/schema.go的237行的ParseWithSpecialTableName函数中进行解析的。如下: image.png

那我们该如何让gorm知道我们的表中主键字段是name呢?那就是通过gorm的tag在model中指定主键,如下:

type MTest02 struct {
	Name string `gorm:"DEFAULT:John;primary_key"`
	Userid int `json:"userid"`
}

这样,gorm就知道了name是表的主键字段,在执行Save函数时,就会根据name字段进行更新了。如下:

UPDATE `m_test_02` SET `userid`=100 WHERE `name` = 'Stone'

如果当表中没有主键或在gorm的Model中没有指定主键字段时,执行Save函数就需要指定具体的Where条件才能进行更新; 当表中的主键字段名非id时,则需要使用gorm:"primary_key"的标签来将model中的字段和表中的非id字段主键进行关联。

三、表中有id字段,但非主键

如果表中存在id字段,但该id字段又非主键,那么save函数会怎么样呢?我们一起来看下。 首先,创建一个表m_test_03,该表中有id字段,但非主键。同时也不存在任何其他主键。如下:

CREATE TABLE `m_test_03` (
  `id` int not null,
  `name` varchar(100) NOT NULL DEFAULT '',
  `userid` int(11) DEFAULT NULL
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;

建立该表对应的model,如下:

type MTest03 struct {
	Id int
	Name string `gorm:"DEFAULT:John"`
	Userid int `json:"userid"`
}

func (m *MTest03) TableName() string {
	return "m_test_03"
}

然后通过Save函数保存如下数据:

dsn := "sands:123456@tcp(test.trdplace.ads.sg1.mysql:3306)/test01?charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms"

db, _ := gorm.Open(mysql.Open(dsn), nil)


var row = MTest03{
	Id: 1,
	Name: "Stone",
	Userid: 100,
}

err := db.Save(&row).Error
fmt.Printf("debug-err:%+v\n", err)

当我们执行该代码后,会发现Save函数依然是转换成了以id为where条件的update语句,如下:

 UPDATE `m_test_03` SET `name`='Stone',`userid`=100 WHERE `id` = 1

你看,我们表中虽然有id字段,并非主键。但gorm依然默认的将id字段作为了主键进行更新

四、总结

Save函数会优先做更新操作。在更新不成功时,再做插入操作。在更新操作时,优先使用字段名为"id"的字段作为主键来进行更新。若表中没有id字段,但存在其他主键字段,则需要在model中通过标签gorm:primary将gorm和表主键字段关联起来。最后,如果model中只包含表的部分字段,那么未包含在内的字段会对应的被更新成对应类型的默认值。

特别说明:你的关注,是我写下去的最大动力。可在Go学堂 中领取《100个go常见的错误》pdf文档、经典go学习资料。