GORM学习笔记

gorm官方有中文文档,但是实际使用时,还是有很多没有详细注意到的地方,这里做一个整理的笔记。

GORM 指南

特性

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

安装

1
go get -u gorm.io/gorm

安装驱动,连接对应数据库使用对应驱动

1
2
3
4
5
6
7
8
// sqlite
go get -u gorm.io/driver/sqlite
// mysql
go get -u gorm.io/driver/mysql
// sql server
go get -u gorm.io/driver/sqlserver
// pgsql
go get -u gorm.io/driver/postgres

例如使用mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package mysql

import (
"log"
"time"

"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local", // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
}

其中,DSN格式如下 username:password@tcp(host:port)/db_name

GORM 使用 database/sql 维护连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sqlDB, err := db.DB()

// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
SetMaxIdleConns设置连接池中的最大闲置连接数。
如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。
如果n <= 0,不会保留闲置连接。

// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)
SetMaxOpenConns设置与数据库建立连接的最大数目。
如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。
如果n <= 0,不会限制最大开启连接数,默认为0(无限制)。

// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)

例如,当使用db没有返回连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
db, err := sql.Open("postgres",
"host=192.168.51.206 user=datatom password=db_password dbname=gorm port=30238 sslmode=disable TimeZone=Asia/Shanghai")
if err != nil {
log.Fatal(err)
}
defer db.Close()

// 最大连接数5个
db.SetMaxOpenConns(5)
err = db.Ping()
if err != nil {
log.Fatal(err)
}

for i := 0; i < 20; i++ {
// 使用事务,但是不提交,占用连接
_, err := db.Begin()
if err != nil {
log.Fatal(err)
}
fmt.Println("conn ", i)
}
}

执行后,会卡在获取第六个连接

1
2
3
4
5
6
conn  0
conn 1
conn 2
conn 3
conn 4

使用gorm也是一样

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
func main() {
dsn := "host=192.168.51.206 user=datatom password=db_password dbname=gorm port=30238 sslmode=disable TimeZone=Asia/Shanghai"
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(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
return
}
s, err := db.DB()
s.SetMaxOpenConns(5)
if err != nil {
return
}

for i := 0; i < 20; i++ {
db.Begin()
if err != nil {
log.Fatal(err)
}
fmt.Println("conn ", i)
}
}

也会卡在获取第6个连接时

1
2
3
4
5
6
conn  0
conn 1
conn 2
conn 3
conn 4

简单实用的操作

打印日志

打印日志的功能非常实用,既可以看到所有raw sql语句,也可以针对慢日志打印语句

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

// 连接使用newLogger的配置
&gorm.Config{
Logger: newLogger,
}

创建表

gorm可以通过迁移,将结构体映射成表

1
2
3
4
type User struct {
gorm.Model
Name string
}
1
2
3
4
err = db.AutoMigrate(User{})
if err != nil {
log.Fatal(err)
}

可以看到日志如下,默认情况,在没有指定表名时,表名为全小写,驼峰体变更为下划线。

1
2
3
4
[1.399ms] [rows:-] SELECT DATABASE()
[6.693ms] [rows:1] SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gorm%' ORDER BY SCHEMA_NAME='gorm' DESC,SCHEMA_NAME limit 1
[6.108ms] [rows:-] SELECT count(*) FROM information_schema.tables WHERE table_schema = 'gorm' AND table_name = 'users' AND table_type = 'BASE TABLE'
[34.257ms] [rows:0] CREATE TABLE `users` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime NULL,`updated_at` datetime NULL,`deleted_at` datetime NULL,`name` varchar(256),PRIMARY KEY (`id`),INDEX `idx_users_deleted_at` (`deleted_at`))

其中,gorm.Model 的定义

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

一般通过结构体嵌套使用

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
gorm.Model
Name string
}
// 等效于
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
}

测试结构体

1
2
3
4
5
6
7
8
9
10
11
type User struct {
ID uint // 默认情况下,会将id字段设置为主键
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}

CRUDL

新增

1
2
3
4
result = db.Create(&User{    // 传入指针,创建后,该类型的id会被更新
Name: "mitaka",
})
// [14.754ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2022-09-26 15:33:49.137','2022-09-26 15:33:49.137',NULL,'mitaka')

返回信息

1
2
3
user.ID             // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数

批量插入

批量插入,可以传入一个切片的指针,插入的时候,是使用一条语句进行创建

1
2
3
4
5
6
7
8
var users = []User{{Name: "mitaka1"}, {Name: "mitaka2"}, {Name: "mitaka3"}}
db.Create(&users)
// 单一sql语句
// [27.023ms] [rows:3] INSERT INTO `users` (`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`) VALUES ('mitaka1',NULL,0,NULL,NULL,NULL,'2022-09-26 16:38:22.93','2022-09-26 16:38:22.93'),('mitaka2',NULL,0,NULL,NULL,NULL,'2022-09-26 16:38:22.93','2022-09-26 16:38:22.93'),('mitaka3',NULL,0,NULL,NULL,NULL,'2022-09-26 16:38:22.93','2022-09-26 16:38:22.93')

for _, user := range users {
fmt.Println(user.ID) // 1,2,3
}

使用 CreateInBatches 分批创建时,可以指定每批的数量(sql语句有长度限制,当数据量很大时,可以使用这个方式)

1
2
3
db.CreateInBatches(&users, 2)
// [3.693ms] [rows:2] INSERT INTO `users` (`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`) VALUES ('mitaka1',NULL,0,NULL,NULL,NULL,'2022-09-26 16:43:31.42','2022-09-26 16:43:31.42'),('mitaka2',NULL,0,NULL,NULL,NULL,'2022-09-26 16:43:31.42','2022-09-26 16:43:31.42')
// [3.590ms] [rows:1] INSERT INTO `users` (`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`) VALUES ('mitaka3',NULL,0,NULL,NULL,NULL,'2022-09-26 16:43:31.423','2022-09-26 16:43:31.423')

第一条语句创建2个,第二条创建1个。

创建钩子,GORM 允许用户定义的钩子有 BeforeSave, BeforeCreate, AfterSave, AfterCreate 创建记录时将调用这些钩子方法,

例如在创建之前,有一些逻辑判断,判断该用户是否有某些权限之类的

1
2
3
4
5
6
7
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {

if u.Role == "admin" {
return errors.New("invalid role")
}
return
}

通过map批量创建

GORM 支持根据 map[string]interface{}[]map[string]interface{}{} 创建记录,例如:

1
2
3
4
5
6
7
8
9
10
11
db.Model(&User{}).Create(map[string]interface{}{
"Name": "mitaka", "Age": 18,
})
// [21.837ms] [rows:1] INSERT INTO `users` (`age`,`name`) VALUES (18,'mitaka')

// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "mitaka1", "Age": 18},
{"Name": "mitaka2", "Age": 20},
})
// [11.139ms] [rows:2] INSERT INTO `users` (`age`,`name`) VALUES (18,'mitaka1'),(20,'mitaka2')

map创建适用于不知道表结构的情况

查询

查询一个,根据主键查询

1
2
3
4
5
6
7
8
9
10
11
12
13
    db.First(&User{}, 1)                    // 根据主键查询
db.First(&User{}, "name = ?", "mitaka") // 根据条件查询
// [2.951ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
// [2.618ms] [rows:1] SELECT * FROM `users` WHERE name = 'mitaka' AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1

// 其他查询
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

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

返回值和错误处理

1
2
3
4
5
6
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

通过主键高级查询

1
2
3
4
5
6
7
8
9
// 根据主键查询多行
var users []User
db.Find(&users, []int{1, 2, 3})
// [5.704ms] [rows:3] SELECT * FROM `users` WHERE `users`.`id` IN (1,2,3)

// 如果主键是字符串(例如像 uuid),查询将被写成这样:(ps:string类型的主键支持这种方式查询是新版功能,老版本会出现只截取整数类型的数值)
var user User
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// [15.420ms] [rows:1] SELECT * FROM `users` WHERE id = '1b74413f-f3b8-409f-ac47-e8c062e3472a' ORDER BY `users`.`id` LIMIT 1

条件查询

1
2
3
4
5
6
7
// Get all records
result := db.Find(&users)
// SELECT * FROM users;

// 返回值
result.RowsAffected // returns found records count, equals `len(users)`
result.Error // returns error

使用string的方式,(当没有设置columntag时,搜索的字段名,大小写不敏感)

1
2
3
4
// 使用string更新
var users []User
db.Where("name = ?", "mitaka").Find(&users) // 通过users,获取到表名
// [5.784ms] [rows:1] SELECT * FROM `users` WHERE name = 'mitaka'

更多的用法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

ps:使用结构体作为查询条件时,依然有零值问题。

更多的查询方式和条件使用,详见官方文档:查询和高级查询

更新

根据主键更新一个

1
2
3
var u User
db.First(&u, 1) // 根据主键查询
db.Model(&u).Update("name", "xiaoyeshiyu")

如果Model不包含主键(只能接受主键条件),则需要条件查询,不然会出现报错,WHERE conditions required

1
db.Model(&User{}).Update("name", "mitaka")

通过Where条件更新

1
2
db.Model(&User{}).Where(User{Name: "xiaoyeshiyu"}).Update("name", "mitaka")
// [5.208ms] [rows:0] UPDATE `users` SET `name`='mitaka',`updated_at`='2022-09-26 15:33:49.157' WHERE `users`.`name` = 'xiaoyeshiyu' AND `users`.`deleted_at` IS NULL

注意,更新可以通过map[string]interface{}更新多个,也可以通过结构体更新,其中,结构体更新仅更新非零字段。(也就是零值不更新)

1
2
3
4
5
type User struct {
gorm.Model
Name string
Age int
}
1
2
db.Model(&u).Updates(User{Name: "mitaka", Age: 0})
// [13.125ms] [rows:0] UPDATE `users` SET `updated_at`='2022-09-26 15:49:08.27',`name`='mitaka' WHERE `users`.`deleted_at` IS NULL AND `id` = 1

可以看到,没有更新Age的语法,此时可以通过map[string]interface{}更新

1
2
3
4
5
db.Model(&u).Updates(map[string]interface{}{
"name": "mitaka",
"age": 0,
})
// [5.152ms] [rows:0] UPDATE `users` SET `age`=0,`name`='mitaka',`updated_at`='2022-09-26 15:49:37.67' WHERE `users`.`deleted_at` IS NULL AND `id` = 1

也可以使用Select强制更新(使用select时,需要指定所有需要更新的字段)

1
2
db.Model(&u).Select("name", "age").Updates(User{Name: "mitaka", Age: 0})
// [5.838ms] [rows:0] UPDATE `users` SET `updated_at`='2022-09-26 15:51:47.907',`name`='mitaka',`age`=0 WHERE `users`.`deleted_at` IS NULL AND `id` = 1

还可以通过sql中的NullInt64类型实现

1
2
3
4
5
type User struct {
gorm.Model
Name string
Age sql.NullInt64
}
1
2
3
4
5
db.Model(&u).Updates(User{Name: "mitaka", Age: sql.NullInt64{
Int64: 0,
Valid: true, // true为更新,false不更新
}})
// [4.523ms] [rows:0] UPDATE `users` SET `updated_at`='2022-09-26 15:56:51.534',`name`='mitaka',`age`=0 WHERE `users`.`deleted_at` IS NULL AND `id` = 1

另外,还可以将字段设置为指针类型,也可以更新零值

1
2
3
4
var empty string
user := User{Name: "mitaka", Email: &empty}
db.Model(User{ID: 1}).Updates(&user)
// [16.577ms] [rows:1] UPDATE `users` SET `name`='mitaka',`email`='',`updated_at`='2022-09-26 17:24:38.672' WHERE `id` = 1

通过save保存所有字段(包括零值),save是一个创建和更新语句

1
2
3
4
5
6
7
8
9
10
11
// 没有主键
var user User
user.Name = "mitaka"
user.Age = 28
db.Save(&user)
// [15.861ms] [rows:1] INSERT INTO `users` (`name`,`email`,`age`,`birthday`,`member_number`,`activated_at`,`created_at`,`updated_at`) VALUES ('mitaka',NULL,28,NULL,NULL,NULL,'2022-09-26 17:13:36.619','2022-09-26 17:13:36.619')

// 有主键
user.ID = 2
db.Save(&user)
// [12.707ms] [rows:1] UPDATE `users` SET `name`='mitaka',`email`=NULL,`age`=28,`birthday`=NULL,`member_number`=NULL,`activated_at`=NULL,`created_at`='2022-09-26 17:15:03.18',`updated_at`='2022-09-26 17:15:03.196' WHERE `id` = 2

删除

根据主键删除(不带主键的话,会触发批量删除)

1
2
    db.Delete(&u, 1)
// [10.681ms] [rows:1] UPDATE `users` SET `deleted_at`='2022-09-26 15:37:42.275' WHERE `users`.`id` = 1 AND `users`.`id` = 1 AND `users`.`deleted_at` IS NULL

不带主键,带条件的批量删除

1
2
db.Where(User{Name: "mitaka"}).Delete(&User{})
// [8.132ms] [rows:1] UPDATE `users` SET `deleted_at`='2022-09-26 15:40:25.535' WHERE `users`.`name` = 'mitaka' AND `users`.`deleted_at` IS NULL

如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!

如果在没有任何条件的情况下执行批量删除,GORM 不会执行该操作,并返回 ErrMissingWhereClause 错误

1
2
3
4
user := User{Name: "mitaka"}
db.Delete(&user)
// WHERE conditions required
// [2.004ms] [rows:0] DELETE FROM `users`

获取被软删除的记录

1
2
3
var users []User
db.Unscoped().Where("name = ?", "mitaka").Find(&users)
// [2.967ms] [rows:5] SELECT * FROM `users` WHERE name = 'mitaka'

类型定义

GORM 倾向于约定,而不是配置。默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。(例如User结构体,迁移后的表名为users,如果是UserInfo,则是user_infos

自定义配置,通过标签配置

1
2
3
4
5
type User struct {
gorm.Model
Name string `gorm:"column:my_name"`
Age sql.NullInt64
}

除了column,还有其他的标签如下:tag 名大小写不敏感,但建议使用 camelCase 风格

标签名 说明
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 specifies column precision,精度
scale specifies column scale,列大小
not null specifies column as NOT NULL,非空
autoIncrement specifies column auto incrementable,自增
autoIncrementIncrement auto increment step, controls the interval between successive column values,自增增量
embedded embed the field,嵌入的结构体
embeddedPrefix column name prefix for embedded fields,嵌入结构体的前缀
autoCreateTime track current time when creating, for int fields, it will track unix seconds, use value nano/milli to track unix nano/milli seconds, e.g: autoCreateTime:nano,自动创建时间,如果是int类型,则是跟踪unix秒
autoUpdateTime track current time when creating/updating, for int fields, it will track unix seconds, use value nano/milli to track unix nano/milli seconds, e.g: autoUpdateTime:milli,自动更新时间
index create index with options, use same name for multiple fields creates composite indexes, refer Indexes for details,索引
uniqueIndex same as index, but create uniqued index,唯一索引
check creates check constraint, eg: check:age > 13, refer Constraints,字段检查
<- set field’s write permission, <-:create create-only field, <-:update update-only field, <-:false no write permission, <- create and update permission,设置写入和更新权限
-> set field’s read permission, ->:false no read permission,设置读取权限
- ignore this field, - no read/write permission, -:migration no migrate permission, -:all no read/write/migrate permission,忽略字段
comment add comment for field when migration,添加注释

多个标签,通过;隔开

1
2
3
4
5
type User struct {
gorm.Model
Name string `gorm:"column:my_name;type:varchar(50)"`
Age sql.NullInt64
}

多表操作

多表操作适用于需要表关联的场景,例如belongs to (一对一属于,能有多个A属于一个B),has one(一对一,一个A只有一个B),has many (一对多,一个A拥有多个B),Many to Many(多对多,一个A属于多个B,一个B有多个A)。

belongs to

1
2
3
4
5
6
7
8
9
10
11
12
// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}

type Company struct {
ID int
Name string
}

创建表

1
2
// 同时新建users表和companies表
db.AutoMigrate(User{})

创建时,则需要确保外键存在,因此,创建时,会优先创建Company,再创建User

1
2
3
4
5
6
7
8
9
user := User{
Name: "mitaka",
Company: Company{
Name: "x",
},
}
db.Create(&user)
// [13.027ms] [rows:1] INSERT INTO `companies` (`name`) VALUES ('x') ON DUPLICATE KEY UPDATE `id`=`id`
// [25.259ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`company_id`) VALUES ('2022-09-26 17:45:32.484','2022-09-26 17:45:32.484',NULL,'mitaka',1)

当外键存在时,可以直接通过id指定

1
2
3
4
5
6
user := User{
Name: "mitaka01",
CompanyID: 1,
}
db.Create(&user)
// [9.861ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`company_id`) VALUES ('2022-09-26 17:47:17.531','2022-09-26 17:47:17.531',NULL,'mitaka01',1)

关联查询

1
2
3
4
var user User
db.Preload("Company").Find(&user, 1) // 放入关联的字段名称
// [4.732ms] [rows:1] SELECT * FROM `companies` WHERE `companies`.`id` = 1
// [10.439ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL

或者通过join方法,相比之下,join是使用的一条语句,使用join操作

1
2
db.Joins("Company").Find(&user, 1)
// [4.426ms] [rows:1] SELECT `users`.`id`,`users`.`created_at`,`users`.`updated_at`,`users`.`deleted_at`,`users`.`name`,`users`.`company_id`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` `Company` ON `users`.`company_id` = `Company`.`id` WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL

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

CompanyID是名称,int是类型。此时如果改成使用其他字段,例如使用CompanyName字段作为外键

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
Name string
CompanyID string // 字段类型改为string
Company Company `gorm:"references:Name"` // 使用 Name 作为引用
}

type Company struct {
ID int
Name string
}

has many

has many 与belongs to在数据库存储上是一样的,只是相对于不同的角度。例如,在User的角度,就是belongs to一个Company,那么在Company的角度,就是一个Company has many 多个User

例如一个用户有多个信用卡,一个信用卡只属于一个用户

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

type CreditCard struct {
gorm.Model
Number string
UserID uint // 这个UserID就是User表中的ID字段
}

创建表,此时需要创建两张表,由于没有特殊指定外键,无法通过UserID字段获取对应结构体字段,因此无法通过只创建CreditCard创建两张表

1
db.AutoMigrate(User{}, CreditCard{})

创建和检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
u := User{
CreditCards: []CreditCard{
{
Number: "a",
},
{
Number: "b",
},
{
Number: "c",
},
},
}
db.Create(&u)

var users []User
db.Model(&User{}).Preload("CreditCards").Find(&users)

重写外键

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"foreignKey:UserRefer"`
}

// CreditCard 表的外键字段是UserRefer,指向User的ID字段
type CreditCard struct {
gorm.Model
Number string
UserRefer uint
}

ps:一般在高并发的系统中,不会使用外键约束,这是由于外键影响性能。一般通过业务层面的代码确保数据完整性,而不是首选外键。

通过user获取CreditCards的查询方式(与belongs to相同的是语法,Preload外键字段,不同的是belongs to用在这里,是通过CreditCards获取所属的User

1
2
3
4
5
var user User
db.Model(&User{}).Preload("CreditCards").First(&user)
for _, cre := range user.CreditCards {
fmt.Println(cre.UserRefer)
}

many to many

例如,您的应用包含了 userlanguage,且一个 user 可以说多种 language,多个 user 也可以说一种 language

这种情况下,无法通过user的外键指向language或者通过language的外键指向user,这种情况下,一般使用第三张表存储两者之间的关系。gorm中,通过many2many创建第三张关联表。

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

type Language struct {
gorm.Model
Name string
}
1
2
db.AutoMigrate(User{}, Language{})
// [17.745ms] [rows:0] CREATE TABLE `user_languages` (`user_id` bigint unsigned,`language_id` bigint unsigned,PRIMARY KEY (`user_id`,`language_id`),CONSTRAINT `fk_user_languages_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),CONSTRAINT `fk_user_languages_language` FOREIGN KEY (`language_id`) REFERENCES `languages`(`id`))

创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
language1 := Language{Name: "go"}
language2 := Language{Name: "python"}
language3 := Language{Name: "php"}

u1 := User{
Languages: []Language{language1, language2},
}
u2 := User{
Languages: []Language{language3, language2},
}
db.Create(&u1)
// [3.321ms] [rows:2] INSERT INTO `languages` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2022-09-27 09:38:15.442','2022-09-27 09:38:15.442',NULL,'go'),('2022-09-27 09:38:15.442','2022-09-27 09:38:15.442',NULL,'python') ON DUPLICATE KEY UPDATE `id`=`id`
// [4.177ms] [rows:2] INSERT INTO `user_languages` (`user_id`,`language_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `user_id`=`user_id`
// [20.401ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`) VALUES ('2022-09-27 09:38:15.435','2022-09-27 09:38:15.435',NULL)
db.Create(&u2)
// [2.758ms] [rows:2] INSERT INTO `languages` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2022-09-27 09:38:15.458','2022-09-27 09:38:15.458',NULL,'php'),('2022-09-27 09:38:15.458','2022-09-27 09:38:15.458',NULL,'python') ON DUPLICATE KEY UPDATE `id`=`id`
// [3.019ms] [rows:2] INSERT INTO `user_languages` (`user_id`,`language_id`) VALUES (2,3),(2,4) ON DUPLICATE KEY UPDATE `user_id`=`user_id`
// [14.102ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`) VALUES ('2022-09-27 09:38:15.456','2022-09-27 09:38:15.456',NULL)

获取

1
2
3
4
5
6
7
8
u := User{}
db.Preload("Languages").Find(&u, 1)
// [4.432ms] [rows:2] SELECT * FROM `user_languages` WHERE `user_languages`.`user_id` = 1
// [4.026ms] [rows:2] SELECT * FROM `languages` WHERE `languages`.`id` IN (1,2) AND `languages`.`deleted_at` IS NULL
// [13.775ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL
for _, l := range u.Languages {
fmt.Println(l.Name)
}

反向引用

1
2
3
4
5
6
7
8
9
10
type User struct {
gorm.Model
Languages []*Language `gorm:"many2many:user_languages;"`
}

type Language struct {
gorm.Model
Name string
Users []*User `gorm:"many2many:user_languages;"`
}

通过这种反向引用的方式,user获取language,则通过Preload Languages,当然,也可以通过language获取user,通过Preload Users

关联模式,关联模式是通过一条语句,通过join实现关联

1
2
3
4
5
var u User
u.ID = 1
var l []*Language
db.Model(&u).Association("Languages").Find(&l)
// [6.577ms] [rows:2] 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

自定义表名

通过给结构体添加TableName方法自定义表名

1
2
3
func (User) TableName() string {
return "myUser"
}

添加表前缀,在配置中指定命名规则

1
2
3
4
5
6
&gorm.Config{
Logger: newLogger,
NamingStrategy: schema.NamingStrategy{
TablePrefix: "my_",
},
}

ps:当既有配置,也有TableName函数时,配置不会生效。

自定义列

例如自定义一个time.time属性的列,在创建时自动增加创建时间

方法1、通过钩子,BeforeCreate、BeforeUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
gorm.Model
Name string
Atime time.Time
}

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.Atime = time.Now()
return nil
}

u := User{
Name: "mitaka",
}
db.Create(&u)
// [12.566ms] [rows:1] INSERT INTO `myUser` (`created_at`,`updated_at`,`deleted_at`,`name`,`atime`) VALUES ('2022-09-27 10:24:25.151','2022-09-27 10:24:25.151',NULL,'mitaka','2022-09-27 10:24:25.151')

需要注意,此时如果没有BeforeCreate的钩子创建时间,保存会出现报错:Error 1292: Incorrect datetime value: '0000-00-00' for column 'atime' at row 1,此时可以将Atime字段类型设置为 sql.NullTime

方法2、使用gorm自带的时间戳,字段名称改为CreatedAt

1
2
3
4
type User struct {
Name string
CreatedAt time.Time
}

还可以将这个字段设置为指针类型

1
2
3
4
5
type User struct {
gorm.Model
Name string
Atime *time.Time
}

优雅的分页

通过一个功能函数封装分页的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page == 0 {
page = 1
}

switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}

offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}

db.Scopes(Paginate(page, pageSize)).Find(&users)

自定义数据结构

在数据库中,无法直接存储切片或者数组,此时需要将切片或者数组转义成字符串,而gORM没有默认支持切片的转义,因此需要针对这种结构体类型,实现自定义转义和读取方式。

1
2
3
4
5
6
7
8
9
10
11
type Images []string

// 实现 sql.Scanner 接口,Scan 将 value 扫描至 Jsonb
func (i *Images) Scan(value interface{}) error {
return json.Unmarshal(value.([]byte), i)
}

// 实现 driver.Valuer 接口,Value 返回 json value
func (i Images) Value() (driver.Value, error) {
return json.Marshal(i)
}

实际的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User1 struct {
gorm.Model

ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
Deleted gorm.DeletedAt
Images Images
}

生成的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `user1` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`name` varchar(256) DEFAULT NULL,
`email` varchar(256) DEFAULT NULL,
`age` tinyint unsigned DEFAULT NULL,
`birthday` datetime DEFAULT NULL,
`member_number` varchar(256) DEFAULT NULL,
`activated_at` datetime DEFAULT NULL,
`deleted` datetime DEFAULT NULL,
`images` longblob,
PRIMARY KEY (`id`),
KEY `idx_user1_deleted_at` (`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

imageslongblob结构。

这里介绍一个工具,SQL转GORM Model,在开发过程中,可以先定义好表结构,然后拷贝SQL语句,转换成结构体。