学习笔记|GORM

本文为实践 gorm 过程中的笔记,也是写给自己的简明教程(看书从厚到薄第一步)。官方文档写的已经很详细了,建议大家伙先查官方文档。

1.ORM

1.1 什么是 ORM

ORM:Object Relational Mapping(对象关系映射)。

作用是在编码中,将对象的概念和数据库中表的概念对应起来。简单讲,定义一个对象,就对应着一张表,一个对象的实例,就对应着一条记录。

1.2 常用 ORM

  • gorm
  • ent
  • sqlx
  • xorm

1.3 ORM 优缺点

优点:

  • 提高开发效率。
  • 屏蔽 SQL 系列,自动将对象的成员变量和表的字段映射,无需直接使用 SQL 编码。
  • 屏蔽各种数据库之间的差异。

缺点:

  • ORM 会牺牲程序性能。
  • 过于依赖 ORM 会导致开发者对于 SQL 理解不够。
  • 过于依赖某个 ORM 会导致切换到其他 ORM 成本过高。

1.4 ORM 和 SQL 之间的关系

  • SQL 为主,ORM 为辅。
  • ORM 的主要目的是为了增加开发效率和代码可维护性。

2.GORM

官方文档:https://gorm.io/zh_CN/docs/index.html

2.1 连接 MySQL

https://gorm.io/zh_CN/docs/connecting_to_the_database.html

1
2
3
4
5
6
7
8
9
10
import (
"gorm.io/driver/mysql" // mysql 驱动
"gorm.io/gorm"
)

func main() {
// 参考 htps://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
  • DSN (Data Source Name) 参数是打开数据库的配置,更多配置可以看 dsn-data-source-name
  • 要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4,查看 此文章 获取详情。

2.2 设置全局的 logger

https://gorm.io/zh_CN/docs/logger.html

1
2
3
4
5
6
7
8
9
10
11
12
13
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 彩色打印
},
)

db, err := gorm.Open(mysql.New(mysql.Config{dsn}), &gorm.Config{
Logger: newLogger,
})
  • 将 Logger 对象作为作为 gorm 的配置参数。

2.3 表结构定义

https://gorm.io/zh_CN/docs/models.html

数据库中表结构,对应着 go 中的 struct。

1
2
3
4
5
type Product struct {
gorm.Model
Code string
Price uint
}

GORM 倾向于约定优于配置。默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。

gorm.Model 包含了四个功能字段,若要使用上述约定,将 gorm.Model 嵌入表结构中即可。

1
2
3
4
5
6
7
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}

2.4 字段标签

声明 model 时,tag 是可选的。 tag 名大小写不敏感,但建议使用 camelCase 风格。

定义表结构时,可以使用 tag 来进行字段级权限控制。注意语法。

具体看这里

标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size定义列数据类型的大小或长度,例如 size: 256
primaryKey将列定义为主键
unique将列定义为唯一键
default定义列的默认值
precision指定列的精度
scale指定列大小
not null指定列为 NOT NULL
autoIncrement指定列为自动增长
autoIncrementIncrement自动步长,控制连续记录之间的间隔
embedded嵌套字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引获取详情
uniqueIndexindex 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment迁移时为字段添加注释

举个例子:

1
2
3
4
type student struct {
UserID uint `gorm:"primarykey"`
Name string `gorm:"column:user_name;type:varchar(50)"`
}

2.5 迁移 schema

https://gorm.io/zh_CN/docs/migration.html

1
2
3
4
err = db.AutoMigrate(&Product{})  // 传入实例化对象的地址,这是指明 db 操作哪张表的方式,后面会多次用到。
if err != nil {
panic(err)
}

AutoMigrate 会根据传入的 struct 创建表、缺失的外键、约束、列和索引。

执行的 SQL 如下:

1
2
3
4
5
6
7
8
9
10
11
12
create table products
(
id bigint unsigned auto_increment primary key,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null,
code varchar(256) null,
price bigint unsigned null
);

create index idx_products_deleted_at
on products (deleted_at);

2.6 自定义表名

需求一:自定义单个的表名

为 struct Product 增加 TableName 函数,在 AutoMigrate 时,就会使用自定义的表名。

1
2
3
func (Product) TableName() string {
return "my_product"
}

需求二:为所有表名增加批量的前缀。

1
2
3
4
5
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{ // 在 gorm config 中增加命名策略的配置。
TablePrefix: "my_", // 所有的表名前加前缀 "my_"
},
})

2.7 快速体验增删改查

https://gorm.io/zh_CN/docs/index.html#快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Create
db.Create(&Product{Code: "D42", Price: 100})


// Read
var p Product
db.First(&p, 1) // 根据整型主键查找
db.First(&p, "code = ?", "D42") // 根据其他字段查找:查找 code 字段值为 D42 的记录


// Update - 更新单个字段:将 product 的 price 更新为 200
db.Model(&p).Update("Price", 200)
// Update - 更新多个字段
db.Model(&p).Updates(Product{Price: 200, Code: "F42"}) // 更新多个字段时,仅更新非零值字段
db.Model(&p).Updates(Product{Price: 200, Code: ""}) // 无法将 Code 更新为空字符串,因为仅更新非零值字段
db.Model(&p).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})


// Delete - 删除 product
db.Delete(&p, 1)

2.8 更新零值问题

为什么更新多个字段时,仅更新非零值?

1
db.Model(&p).Updates(Product{Price: 200, Code: ""})

在初始化的 Product 对象中,未明确值的字段,均为零值。当前的需求是想只更新 Price 和 Code 字段,所以其他未明确值的字段会被忽略,也就是零值字段不会被忽略掉。所以如果 Price 和 Code 有零值字段,也会一并被忽略掉。

通过 NullString 解决更新零值问题

对于需要设置成空字符串的字段,gorm 提供了 NullString 类型,NullString 定义如下,实际上是封装了一个 string,并使用一个 bool 变量表示该字段为空字符串还是未定义(未定义就是 NULL)。

1
2
3
4
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}

同理的还有 NullBool、NullByte、NullInt32 等结构体类型,他们在 database/sql 包中。

举个例子:

1
2
3
4
5
6
7
8
9
10
type Product struct {
gorm.Model
Code sql.NullString // 需要设置成空字符串的字段,类型改为 sql.NullString
Price uint
}

// 在删除的时候,传入 sql.NullString 结构体变量,将 Valid 设为 true,表示非 NULL。
var p Product
db.First(&p, "Price = ?", 200) // 查找 Price=200 的记录
db.Model(&p).Updates(Product{Price: 2000, Code: sql.NullString{"", true}}) // 并将该记录的 Price 和 Code 进行修改,其中 Code 是零值。

通过指针解决更新零值问题

1
2
3
4
5
6
7
8
9
10
type Product struct {
gorm.Model
Code *string // 需要设置成空字符串的字段,类型改为指针类型
Price uint
}

var p Product
db.First(&p, "price = ?", 100)
empty := ""
db.Model(&p).Updates(Product{Code: &empty, Price: 200}) // 传入空字符串变量的地址

3.Create 创建

https://gorm.io/zh_CN/docs/create.html

3.1 根据 struct 创建

1
2
3
4
5
6
u := User{Name:"Tom", Age: 18}
result := db.Create(&u) // 传入地址

fmt.Println(u.ID) // 返回插入数据的主键,只有在调用 Create 方法之后才会自动生成 ID
fmt.Println(result.Error) // 返回错误结果
fmt.Println(result.RowsAffected) // 返回影响了多少行

3.2 根据 slice 批量创建

和一条一条数据传递给 mysql 处理的方式相比,批量创建是使用一条 sql,效率更高。

1
2
3
4
5
6
7
8
users := []User{{Name:"Tom", Age: 18}, {Name:"Bob", Age: 20}}
result := db.Create(&users) // 传入 slice 地址

for _, user := range users {
fmt.Println(user.ID) // 返回插入数据的主键,只有在调用 Create 方法之后才会自动生成 ID
}
fmt.Println(result.Error) // 返回错误结果
fmt.Println(result.RowsAffected) // 返回影响了多少行

3.3 分批创建

使用 CreateInBatches 分批创建时,你可以指定每批的数量。

为什么要分批次?因为批量创建是使用一条 sql 进行创建。但一条 sql 是有长度限制的,所以需要进行分批创建。

1
2
3
4
var users = []User{{name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}

// 一次最多创建 100 个
db.CreateInBatches(users, 100)

3.4 根据 map 创建

db.Model(&User{}) 会返回一个 db 指针。既可以单个创建也可以批量创建。

1
2
3
4
5
6
result := db.Model(&User{}).Create(map[string]interface{}{
"name": "Tom", "age": 18,
})

fmt.Println(result.Error) // 返回错误结果
fmt.Println(result.RowsAffected) // 返回影响了多少行
1
2
3
4
5
6
7
8
// 批量创建
result := db.Model(&User{}).Create([]map[string]interface{}{
{"name": "Tom000", "age": 18},
{"name": "Bob000bj=-", "age": 20},
})

fmt.Println(result.Error) // 返回错误结果
fmt.Println(result.RowsAffected) // 返回影响了多少行

3.5 默认值

通过 default 为字段定义默认值。

1
2
3
4
5
type User struct {
ID int64
Name string `gorm:"default:Anonymous"`
Age int64 `gorm:"default:18"`
}

4.Query 查询

https://gorm.io/zh_CN/docs/query.html

4.0 传入参数不要多次使用

一定要注意的地方。

1
2
3
4
5
var u0 User
db.Find(&u0, 1) // 查询的是 id = 1
// SELECT * FROM `users` WHERE `users`.`id` = 1
db.Find(&u0, 2) // 想查询的是 id = 2,实际上查询的是 id = 1 AND id = 2
// SELECT * FROM `users` WHERE `users`.`id` = 2 AND `users`.`id` = 1

u0 是传出参数,在执行完 db.Find(&u0, 1) 之后,u0 会带着这个条件 id = 1,重复使用传入参数 u0 时,则会带着这个条件继续查询。

4.1 检索单个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取第一条记录(主键升序)
var u1 User
db.First(&u1)
fmt.Println(u1) // u 是传出参数,查询到的值保存在 u 中。
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1


// 获取第一条记录,不指定排序字段
var u2 User
db.Take(&u2)
// SELECT * FROM `users` LIMIT 1


// 获取最后一条记录(主键降序)
var u3 User
db.Last(&u3)
// SELECT * FROM `users` ORDER BY `users`.`id` DESC LIMIT 1

如果查不到,怎么办?

1
2
3
4
5
6
7
var u1 User
result := db.First(&u1)
fmt.Println(result.Error) // 返回错误结果,没错误返回 nil
fmt.Println(result.RowsAffected) // 返回影响了多少行
if errors.Is(result.Error, gorm.ErrRecordNotFound) { // 未查询到会返回 gorm.ErrRecordNotFound
fmt.Println("Not Found")
}

4.2 根据主键检索

1
2
3
4
5
6
7
8
9
10
11
12
// 根据主键检索
var u4 User
db.First(&u4, 10) // 默认搜索 '主键 == 10' 的记录
// SELECT * FROM `users` WHERE `users`.`id` = 10 ORDER BY `users`.`id` LIMIT 1
// 10 也可以使用 "10"
// db.First(&u4, "10") // int 也可以用 string 传入


// 根据主键范围查找
var u5 User
db.Find(&u5, []int{1, 2, 3}) // 传递数组,表示主键在该数组范围内。
// SELECT * FROM `users` WHERE `users`.`id` IN (1,2,3)

4.3 检索全部对象

1
2
3
4
5
6
7
8
9
// 检索全部对象
var users []User
result := db.Find(&users) // 全部记录会保存在 users 这个传出参数里

for _, u := range users {
fmt.Println(u.ID, u.Age)
}

fmt.Println("总行数:", result.RowsAffected)

4.4 条件查询

where 的常见用法,和 sql 类似。

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
// 获取第一条匹配的记录,三种写法:string、struct、map
var u6 User
db.Where("name = ?", "Tom").First(&u6)
// SELECT * FROM `users` WHERE name = 'Tom' ORDER BY `users`.`id` LIMIT 1
var u66 User
db.Where(&User{Name: "Tom"}).First(&u66) // 相同功能的写法,传入 User 结构体。
var u666 User
db.Where(map[string]interface{}{"Name": "Tome"}).First(&u666) // 相同功能的写法,传入 map。

// 获取全部的匹配的记录
var u7 []User
db.Where("name <> ?", "Tom").Find(&u7) // 查询全部匹配的记录,应该传入一个数组。如果传入一个结构体变量,只会保存第一条记录。
// SELECT * FROM `users` WHERE name <> 'Tom'

// IN
var u8 []User
db.Where("name IN ?", []string{"Tom", "Bob"}).Find(&u8)
// SELECT * FROM `users` WHERE name IN ('Tom','Bob')

// LIKE
var u9 []User
db.Where("name LIKE ?", "%Bob%").Find(&u9)
// SELECT * FROM `users` WHERE name LIKE '%Bob%'

// AND
var u10 User
db.Where("name = ? AND age = ?", "Tom", 18).First(&u10)
// SELECT * FROM `users` WHERE name = 'Tom' AND age = '18' ORDER BY `users`.`id` LIMIT 1

// Time
var u11 []User
db.Where("updated_at > ?", "2000-01-01 00:00:00").Find(&u11)
// SELECT * FROM `users` WHERE updated_at > '2000-01-01 00:00:00'

// Between
var u12 []User
db.Where("age BETWEEN ? AND ?", 10, 20).Find(&u12)
// SELECT * FROM `users` WHERE age BETWEEN 10 AND 20

// or 条件, 三种写法:string、struct、map
var u13 []User
db.Where("name = ?", "Tom").Or("name = ?", "Bob").Find(&u13)
// SELECT * FROM `users` WHERE name = 'Tom' OR name = 'Bob'

var u133 []User
db.Where(User{Name: "Tom"}).Or(User{Name: "Bob"}).Find(&u133)
// SELECT * FROM `users` WHERE `users`.`name` = 'Tom' OR `users`.`name` = 'Bob'

var u1333 []User
db.Where(map[string]interface{}{"name": "Tom"}).Or(map[string]interface{}{"name": "Bob"}).Find(&u1333)
// SELECT * FROM `users` WHERE name = 'Tom' OR name = 'Bob'

需要注意的地方:

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
/*
*【1.字段名是大小写不敏感的】。
*/
db.Where("name = ?", "Tom").First(&u6)
db.Where("Name = ?", "Tom").First(&u6) // 与前者效果一致


/*
*【2.字段名是数据库中的名称,不是 struct 的结构体变量名】。
*/
type User struct {
MyName string `gorm:"column:name"` // 使用 tag 设置数据库中列名为 name
}
// db.Where("MyName = ?", "Tom").First(&u6) // 错误!无法找到 my_name 这个列
db.Where("name = ?", "Tom").First(&u6) // 正确!使用 数据库列名称


/*
*【3.struct 查询会忽略零值,string 和 map 的方式查询不会忽略零值】。
*/
var u6 User
db.Where("name = ?", "").First(&u6) // string 方式,不会忽略零值
// SELECT * FROM `users` WHERE name = '' ORDER BY `users`.`id` LIMIT 1

var u66 User
db.Where(&User{Name: ""}).First(&u66) // struct 式,忽略零值
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

var u666 User
db.Where(map[string]interface{}{"Name": ""}).First(&u666) // map 方式,不会忽略零值
// SELECT * FROM `users` WHERE `Name` = '' ORDER BY `users`.`id` LIMIT 1

其他和 where 方法类似功能的用法,自行查看 内联条件Not 条件Limit & OffsetGroup By & Having 等。

4.5 高级查询

本质上还是需要 sql 的功底:group、子查询、having、多表查询。

5. Update 更新

https://gorm.io/zh_CN/docs/update.html

5.1 保存所有字段

Save 集 update 和 insert 为一体的函数。

更新:

保存所有字段意味着,

  • 更改的字段:更改后的值,即使更改为零值。
  • 未更改的字段:保持原来的值。

字段比较少的时候,可以选择这种方法。

1
2
3
4
5
6
7
8
9
10
11
12

// 需要先查询出,需要更新的记录。
var u User
db.First(&u)

// 设置值
u.Name = "Tom_001"
u.Age = 0 // 可以更新零值

// 保存所有字段
db.Save(&u)
// UPDATE `users` SET `name`='Tom_001',`email`='11@qq.com',`age`=0,`birthday`=NULL,`member_number`=NULL,`activated_at`=NULL,`created_at`='2022-02-19 17:15:06.408',`updated_at`='2022-03-22 23:44:39.833' WHERE `id` = 1

插入:

更新之前,需要查询出需要更改的记录。如果不查询,就默认为插入操作。

1
2
3
4
5
6
var u User

u.Name = "Tom_001"
u.Age = 0
db.Save(&u)
// INSERT INTO `users` (`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`) VALUES ('Tom_001',NULL,0,NULL,NULL,NULL,'2022-03-22 23:48:14.375','2022-03-22 23:48:14.375') RETURNING `id`

5.2 更新单个字段

Update 可以更新零值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 根据条件选择值,更新
db.Model(&User{}).Where("name = ? ", "Tom").Update("name", "Tom_002")
// UPDATE `users` SET `name`='Tom_002',`updated_at`='2022-03-23 00:04:09.841' WHERE name = 'Tom'


// 根据 id 选择值,更新
db.Frist(&u) // 先查询
db.Model(&u).Update("name", "Tom_002") // 再更改
// UPDATE `users` SET `name`='Tom_002',`updated_at`='2022-03-23 00:08:09.696' WHERE `id` = 1


// 根据 id 和条件选择值,更新
db.Model(&u).Where("Age > ?", 20).Update("name", "Tom_002")
// UPDATE `users` SET `name`='Tom_002',`updated_at`='2022-03-23 00:08:40.8' WHERE Age > 20 AND `id` = 1

5.3 更新多个字段

Updates 不能更新非零值。原因在「更新零值问题」中阐述过。

1
2
3
4
5
6
7
8
9
10
var u User
db.First(&u)

// 根据 strut 更新
db.Model(&u).Updates(User{Name: "Tom", Age: 10})
// UPDATE `users` SET `name`='Tom',`age`=10,`updated_at`='2022-03-23 00:19:02.321' WHERE `id` = 1

// 根据 map 更新
db.Model(&u).Updates(map[string]interface{}{"name": "Tom", "age": 10})
// UPDATE `users` SET `age`=10,`name`='Tom',`updated_at`='2022-03-23 00:19:02.406' WHERE `id` = 1

5.4 更新选定的字段

  • select:只更新 Select 选中的字段【可以更新零值字段】
  • Omit:忽略选中的字段
1
2
db.Model(&u).Select("name", "age").Updates(User{Name: "Tom", Age: 0})
// UPDATE `users` SET `name`='Tom',`age`=0,`updated_at`='2022-03-23 00:26:06.779' WHERE `id` = 1

5.5 更新 Hook

GORM 支持的 hook 点包括:BeforeSave, BeforeUpdate, AfterSave, AfterUpdate

例子:更新记录前,判断要更新的年龄是否合法。

1
2
3
4
5
6
func (u *User) BeforeUpdate(tx *gorm.DB)(err error)  {
if u.Age < 18 {
return errors.New("be Not allowed to update")
}
return
}

尝试更新,返回值得到预期的错误。

1
2
3
4
var u User
db.First(&u)
result := db.Model(&u).Updates(User{Name: "Tom", Age: 10})
fmt.Println(result.Error, result.RowsAffected) // be Not allowed to update 0

5.6 批量更新

如果不通过 Model 指定记录的主键,则 GORM 会执行批量更新。

1
2
3
4
5
6
7
8
// 根据 struct 更新
db.Model(&User{}).Where("name = ?", "Tom").Updates(User{Name: "Tom_001", Age: 28})
// UPDATE `users` SET `name`='Tom_001',`age`=28,`updated_at`='2022-03-23 00:44:12.596' WHERE name = 'Tom'


// 根据 map 更新
db.Model(&User{}).Where("name = ?", "Tom").Updates(map[string]interface{}{"name": "Tom_001", "age": 28})
// UPDATE `users` SET `age`=28,`name`='Tom_001',`updated_at`='2022-03-23 00:45:26.133' WHERE name = 'Tom'

6.Delete 删除

https://gorm.io/zh_CN/docs/delete.html

6.1 删除单条记录

删除对象时指定主键或通过 where 查询。

1
2
3
4
5
6
7
8
9
10
11
12
var u User
db.First(&u)
db.Delete(&u) // 传入主键
// DELETE FROM `users` WHERE `users`.`id` = 1


db.Delete(&User{}, 10) // 传入主键
// DELETE FROM users WHERE id = 10;


db.Where("name = ?", "Tom_002").Delete(&User{}) // 通过 where 查询
// DELETE FROM `users` WHERE name = 'Tom_002'

6.2 Delete Hook

注意:hook 只能在传入结构体时才生效。(至于为啥不知道。)

1
2
3
4
5
6
var u User
db.First(&u)

db.Delete(&u) // hook 可以生效
db.Delete(&User{}, 28) // hook 不生效
db.Where("name = ?", "Tom100").Delete(&User{}) // hook 不生效

hook 写法:判断非法返回一个 error。

1
2
3
4
5
6
func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
if u.Name == "Tom100" {
return errors.New("admin user not allowed to delete")
}
return
}

6.3 软删除

也叫逻辑删除:https://gorm.io/zh_CN/docs/delete.html#软删除

gorm.Model 中包含 gorm.deletedat 字段,表结构定义时,包含 gorm.Model 则自动获得软删除的能力。

拥有软删除能力的模型调用 Delete 时,其实调用的事 Update,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且不能再通过普通的查询方法找到该记录。

7.关联

7.1 Belong to

https://gorm.io/zh_CN/docs/belongs_to.html

belongs to 会与另一个模型建立了一对一的连接。

比如每个 user 对应一个 company,外键放在 user 表中。

7.1.1 关联创建表

创建 users 表时,会自动创建 companies 表。

1
2
3
4
5
6
7
8
9
10
11
12
13
type User struct {
gorm.Model
Name string
CompanyID int // 外键
Company Company // 该字段并不是直接保存在 user 表中,go 中使用嵌套类型,便于 User 结构体直接使用 Company 中变量。
}

type Company struct {
ID int
Name string
}

db.AutoMigrate(&User{}) // 只需迁移 User,自动创建 users 表和 companies 表。

7.1.2 关联插入

1
2
3
4
5
6
7
8
9
db.Create(&User{
Name: "Tom",
Company: Company{Name: "Apple"}, // 先创建 companies 表记录
}) // 再创建 users 表记录

db.Create(&User{
Name: "Bob",
CompanyID: 2, // 已存在改 companies 记录,直接设置外键。
})

7.1.3 重写外键

默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字。

一个 User 实体属于 Company 实体,那么外键的名字一般使用 CompanyID,代码可读性更好。

也可以自定义外键名字的方式,需要使用标签指明。

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
Name string
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"` // 指明使用 CompanyRefer 作为外键。
}

type Company struct {
ID int
Name string
}

7.1.4 关联查询

虽然 User 结构体中嵌套了 Company 结构体,但是查询时,只会 select Users 表,需要关联查询才能查到 Company 的信息。

1
2
3
4
var u User
db.First(&u) // 只会 select Users 表
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
fmt.Println(u.Company.Name)

Preload 预加载

https://gorm.io/zh_CN/docs/preload.html

默认情况下,是预加载全部。也可以使用 内联条件 进行带条件的预加载。

Perload 参数填 user 结构体中嵌套的结构体变量名。

1
2
3
4
5
6
7
8
9
10
11
12
var u User
db.Preload("Company").First(&u) // 两次查询
// SELECT * FROM `companies` WHERE `companies`.`id` = 1
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
fmt.Println(u.Company.Name)


// 带条件的预加载
db.Preload("Company", "name = ?", "Apple").First(&u)

// Where 是用来条件查询 users 表的,Preload 使用内联条件查询 companies 表
db.Where("name = ?", "Tom").Preload("Company", "name = ?", "Apple").First(&u)

Joins 预加载

和 Preload 的区别是,Joins 预加载是使用 left join 的方式加载关联数据。

Joins 只适合 Belong to 和 Has one。

1
2
3
4
var u User
db.Joins("Company").First(&u) // 一次查询
// SELECT `users`.`id`,`users`.`created_at`,`users`.`updated_at`,`users`.`deleted_at`,`users`.`name`,`users`.`company_refer`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` `Company` ON `users`.`company_refer` = `Company`.`id` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
fmt.Println(u.Company.Name)

7.2 Has many

https://gorm.io/zh_CN/docs/has_many.html

has many 与另一个模型建立了一对多的连接。

比如每个 user 可以有多张 credit card,外键放在 credit_card 表中。

7.2.1 创建关联表

1
2
3
4
5
6
7
8
9
10
11
12
13
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCards []CreditCard
}

type CreditCard struct {
gorm.Model
Number string
UserID uint // 外键
}

db.AutoMigrate(&User{}, &CreditCard{}) // 需要同时生成两张表,才会自动建立外键。

7.2.2 关联插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 按照 user struct 的逻辑创建,即可关联创建。
db.Create(&User{ // 先创建 user 记录,再带着 UserID 这个外键值 创建 credit_cards 记录
CreditCards: []CreditCard{
{Number: "001"},
{Number: "002"},
},
})
// INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`) VALUES ('2022-04-02 01:35:54.392','2022-04-02 01:35:54.392',NULL) RETURNING `id`
// INSERT INTO `credit_cards` (`number`,`user_id`) VALUES ('2022-04-02 01:35:54.427','2023-04-02 01:35:54.427',NULL,'001',3),('2022-04-02 01:35:54.427','2022-04-02 01:35:54.427',NULL,'002',3) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`) RETURNING `id`


// 只创建 credit_cards 记录,并指定外键值。
db.Create(&CreditCard{
Number: "003",
UserID: 1, // 指定外键
})

7.2.3 重写外键

自定义外键名称,和 Belong to 一样,标签写在嵌入的结构体变量后。

1
2
3
4
5
6
7
8
9
10
11
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"foreignKey:UserRefer"`
}

type CreditCard struct {
gorm.Model
Number string
UserRefer uint
}

7.2.4 关联查询

这里只能使用 Preload。(Jonis 只适合 Belong to 和 Has one)

Preload 参数为 User 结构体中,嵌套的 CreditCards 结构体变量名。

1
2
3
4
5
6
var u User
db.Preload("CreditCards").First(&u)
for _, c := range u.CreditCards {
fmt.Println(c.Number, c.UserID)
}

7.3 Many to many

https://gorm.io/zh_CN/docs/many_to_many.html

Many to Many 会在两个 model 中添加一张连接表。

比如每个 user 可以会多门 language,一门 language 也可以被多个 user 使用。

7.3.1 创建关联表

使用标签指定 many to many 的关系,并会自动创建连接表,连接表中有两个外键,分别是 User 和 Language。

1
2
3
4
5
6
7
8
9
10
11
12
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"` // 指定 many to many 的关系
}

type Language struct {
gorm.Model
Name string
}

db.AutoMigrate(&User{}, &Language{}) // 自动创建 many to many 的连接表。

7.3.2 关联插入

1
2
3
4
5
6
7
8
9
10
11
12
// 按照 User 结构体的逻辑创建即可。
db.Create(&User{
Languages: []Language{
{Name: "golang"},
{Name: "java"},
},
})
// 先创建 user 记录,再创建 language 记录,最后创建 连接表 记录。

// INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`) VALUES ('2022-04-02 02:13:47.389','2022-04-02 02:13:47.389',NULL) RETURNING `id`
// INSERT INTO `languages` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2022-04-02 02:13:47.423','2022-04-02 02:13:47.423',NULL,'golang'),('2022-04-02 02:13:47.423','2022-04-02 02:13:47.423',NULL,'java') ON DUPLICATE KEY UPDATE `id`=`id` RETURNING `id`
// INSERT INTO `user_languages` (`user_id`,`language_id`) VALUES (3,6),(3,7) ON DUPLICATE KEY UPDATE `user_id`=`user_id`

7.3.3 关联查询

Has many 一样,使用 Preload。

1
2
3
4
5
6
7
8
9
10
var u User
db.Preload("Languages").First(&u)
for _, language := range u.Languages {
fmt.Println(language.Name)
}

// 会查三张表。
// SELECT * FROM `user_languages` WHERE `user_languages`.`user_id` = 1
// SELECT * FROM `languages` WHERE `languages`.`id` IN (1,2) AND `languages`.`deleted_at` IS NULL
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1

7.4 关联模式

https://gorm.io/zh_CN/docs/associations.html

关联模式包含一些在处理关系时有用的方法。

many to many 为例。

7.4.1 查找关联

已经查到 user,需要查询该 user 的全部 language 数据。

1
2
3
4
5
6
7
8
9
10
var u User
db.First(&u)

// 查看 u 的关联数据 languages。
var l []Language
db.Model(&u).Association("Languages").Find(&l)
for _, language := range l {
fmt.Println(language.Name)
}
//SELECT `languages`.`id`,`languages`.`created_at`,`languages`.`updated_at`,`languages`.`deleted_at`,`languages`.`name` FROM `languages` JOIN `user_languages` ON `user_languages`.`language_id` = `languages`.`id` AND `user_languages`.`user_id` = 1 WHERE `languages`.`deleted_at` IS NULL

7.4.2 添加关联

已经查到 user,需要新增该 user 的全部 language 数据。

1
2
3
var u User
db.First(&u)
db.Model(&u).Association("Languages").Append([]Language{{Name: "python"}, {Name: "html"}})

替换关联、删除关联、清空关联等等不再赘述,见官方文档


学习笔记|GORM
https://www.aimtao.net/gorm/
Posted on
2022-07-01
Licensed under