GORM框架

GORM框架

一、声明模型

1.1、使用标准Go结构体(结构体中对象类型必须实现了Scanner和Valuer接口的自定义类型及其指针或者别名组成)

1
2
3
4
5
6
7
8
9
10
11
12
13
//结构体示例
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}

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

gorm.Model:GORM 定义一个 gorm.Model 结构体,其包括字段 IDCreatedAtUpdatedAtDeletedAt

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"`
}

1.2、字段级权限控制

可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略

注意: 使用 GORM Migrator 创建表时,不会创建被忽略的字段

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
Name string `gorm:"<-:create"` // allow read and create
Name string `gorm:"<-:update"` // allow read and update
Name string `gorm:"<-"` // allow read and write (create and update)
Name string `gorm:"<-:false"` // allow read, disable write permission
Name string `gorm:"->"` // readonly (disable write permission unless it configured)
Name string `gorm:"->;<-:create"` // allow read and create
Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
Name string `gorm:"-"` // ignore this field when write and read with struct
Name string `gorm:"-:all"` // ignore this field when write, read and migrate with struct
Name string'gorm:“-:migration”'// ignore this field when migrate with struct
}

读:-> 写(允许读):<-:是哪种写(不标明则默认一般读写)

1.3、创建/更新时间追踪(纳秒、毫秒、秒、Time)

GORM 约定使用 CreatedAtUpdatedAt (表名:createat、update_at)追踪创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间

要使用不同名称的字段,您可以配置 autoCreateTimeautoUpdateTime 标签

如果您想要保存 UNIX(毫/纳)秒时间戳,而不是 time,您只需简单地将 time.Time 修改为 int 即可

1
2
3
4
5
6
7
type User struct {
CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
UpdatedAt int // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
Updated int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳填纳秒数充更新时间
Updated int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
Created int64 `gorm:"autoCreateTime"` // 使用时间戳秒数填充创建时间
}

1.4、嵌入结构体

  1. 匿名字段,GORM 会将其字段包含在父结构体中,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
//实际的model类型
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. 对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Author struct {
Name string
Email string
}

type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
  1. 列名增加前缀 embeddedPrefix 来为 db 中的字段名添加前缀,例如:
1
2
3
4
5
6
7
8
9
10
11
12
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}

1.5、字段标签

声明 model 时,tag 是可选的,GORM 支持以下 tag: tag 名大小写不敏感,但建议使用 camelCase 风格

标签名 说明
column 指定 db 列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsizeautoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer specifies serializer for how to serialize and deserialize data into db, e.g: serializer:json/gob/unixtime –制定序列化器
size specifies column data size/length, e.g: size:256 指定一个单元格长度
primaryKey specifies column as primary key–指定主键
unique specifies column as unique–指定唯一
default specifies column default value–指定默认值
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
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 –自动更新时间(time.Time时间戳、int毫秒数)
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–列名备注

二、链接数据库

2.1、链接MySql

2.1.1、常规链接

  1. 声明数据库连接地址
  2. 使用func Open(dialector Dialector, opts ...Option) (db *DB, err error)打开链接
    1. 传递参数:
      1. 链接地址连接器使用MySql连接器链接

mysql.Open(dsn)源码

1
2
3
func Open(dsn string) gorm.Dialector {
return &Dialector{Config: &Config{DSN: dsn}}
}
  2. 配置选项使用MySql配置

gorm.Config{} 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
32
33
34
35
36
37
38
39
40
41
type Config struct {
// GORM perform single create, update, delete operations in transactions by default to ensure database data integrity
// You can disable it by setting `SkipDefaultTransaction` to true
SkipDefaultTransaction bool
// NamingStrategy tables, columns naming strategy
NamingStrategy schema.Namer
// FullSaveAssociations full save associations
FullSaveAssociations bool
// Logger
Logger logger.Interface
// NowFunc the function to be used when creating a new timestamp
NowFunc func() time.Time
// DryRun generate sql without execute
DryRun bool
// PrepareStmt executes the given query in cached statement
PrepareStmt bool
// DisableAutomaticPing
DisableAutomaticPing bool
// DisableForeignKeyConstraintWhenMigrating
DisableForeignKeyConstraintWhenMigrating bool
// DisableNestedTransaction disable nested transaction
DisableNestedTransaction bool
// AllowGlobalUpdate allow global update
AllowGlobalUpdate bool
// QueryFields executes the SQL query with all fields of the table
QueryFields bool
// CreateBatchSize default create batch size
CreateBatchSize int

// ClauseBuilders clause builder
ClauseBuilders map[string]clause.ClauseBuilder
// ConnPool db conn pool
ConnPool ConnPool
// Dialector database dialector
Dialector
// Plugins registered plugins
Plugins map[string]Plugin

callbacks *callbacks
cacheStore *sync.Map
}
  1. 链接代码示例
1
2
3
4
5
6
7
8
9
10
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func main() {
// 参考 https://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{})
}

2.1.2、自定义驱动链接

1
2
3
4
5
6
7
8
9
import (
_ "example.com/my_mysql_driver" //自定义实现mysql驱动
"gorm.io/gorm"
)

db, err := gorm.Open(mysql.New(mysql.Config{
DriverName: "my_mysql_driver",//这里指定驱动名称
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", // Data Source Name,参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name
}), &gorm.Config{})

2.1.3、使用现有的数据库驱动

实质上是自定义创建一个自定义的Mysql配置连接器进行连接

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

sqlDB, err := sql.Open("mysql", "mydb_dsn") //现有的数据库驱动
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB, //传入一个初始化链接
}), &gorm.Config{})

三、CURD

3.1、CREATE

3.1.1、使用Model对象创建

由于Gorm也是根据ORM对象来对实际的记录进行增删改的修改操作,因此创建插入记录方式如下:

  1. 单条记录
    1. 全量创建记录
1
2
3
4
5
6
7
8
//1、声明一个ORM数据
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
//2、通过数据的指针来创建对象信息(由于需要存储插入后的返回信息因此需要传入对象指针)
result := db.Create(&user)
//3、获取相应的数据
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
  1. 指定字段创建记录
    1. 按照传入字段插入Select + Create
1
2
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")
  2. 除去传入字段插入Omit(省略) + Create
1
2
db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")
  1. 批量插入
    1. 一次性全量插入–使用slice切片进行传递创建的对象数组
1
2
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
  1. 分批插入–使用slice传递并且调用CreateInBatches
1
db.CreateInBatches(users, 100)

⚠️注:使用批量插入的过程中,加入在初始化Mysql链接的时候创建了批量的session那么带着这个对象进行的CURD均遵循批量的方法

1
2
3
4
5
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
CreateBatchSize: 1000,
})

db := db.Session(&gorm.Session{CreateBatchSize: 1000})

3.1.2、使用Map创建

不会回传主键,并且不存在association级链插入

Go Map创建方法:

1
2
3
4
5
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
  1. Map单一创建
1
2
3
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})
  1. Map批量创建
1
2
3
4
5
// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})

3.2.3、关联创建

    创建关联数据时,如果关联值是非零值,这些关联会被 upsert,且它们的 Hook 方法也会被调用(级链查询)–使用Select或者Omit进行选择插入或者忽略插入即可。当然也可以使用default默认值进行插入创建。

不声明默认值,会导致像 0''false 等零值保存到数据库(当进行整个结构体查询时 gorm会默认把没有设置的值为0 所以无法进行查询 gorm只能进行非零字段查询。(不推荐结构体查询))

  1. 使用default标签则不会
    2.将结构体中的int改为指针int类型(零值类型相同处理方法)
    3.Scanner/Valuer:Scanner.Scan()方法为读取数据库内容转化为自己格式。 Valuer.Value()转化自己的结构体到SQL能读取的格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type CreditCard struct {
gorm.Model
Number string
UserID uint
}

type User struct {
gorm.Model
Name string
CreditCard CreditCard
}

db.Create(&User{
Name: "jinzhu",
CreditCard: CreditCard{Number: "411111111111"}
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...

3.2、UPDATE

3.3、RETRIEVE

3.3.1、查询单个对象

    GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误—->>> 避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user)Find方法可以接受struct和slice的数据,传入结构体会出现这个错误,但是传入切片则不会出现这个错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

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

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

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

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

First和Last自动根据主键排序,但是只有当目标是结构体指针或者通过db.Model()指定ORM模型的时候才有效,并且若没有定义主键,那么就只会按照Model第一个字段排序。

结构体定义

1
2
var user User
var users []User
  1. 生效写法 – 指定Model或者使用结构体绑定
1
2
3
4
5
6
7
8
// works because destination struct is passed in
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// works because model is specified using `db.Model()`
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
  1. 不生效写法
1
2
3
// doesn't work
result := map[string]interface{}{}
db.Table("users").First(&result)
  1. 不指定主键(自动使用结构体第一个字短)
1
2
3
4
5
6
7
// no primary key defined, results will be ordered by first field (i.e., `Code`)
type Language struct {
Code string
Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1

解决ErrorNotFound错误:(源码追溯)—->>>> 对这个err做多一步的处理

1
2
3
4
5
6
7
db.Where("****").Find(&user).Error
if err == gorm.ErrRecordNotFound {
// do something
}
if err != nil {
// do something
}

3.4、DELETE

3.5、使用钩子函数

    Gorm调用过程中具有完整的生命周期,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。

编写方法:让相应的表实体类绑定相应实现的钩子函数方法即可

1
2
3
4
5
6
7
8
9
10
11
12
func (u *User) BeforeCreate(tx *gorm.DB) error {
// 通过 tx.Statement 修改当前操作,例如:
tx.Statement.Select("Name", "Age")
tx.Statement.AddClause(clause.OnConflict{DoNothing: true})

// tx 是带有 `NewDB` 选项的新会话模式
// 基于 tx 的操作会在同一个事务中,但不会带上任何当前的条件
err := tx.First(&role, "name = ?", user.Role).Error
// SELECT * FROM roles WHERE name = "admin"
// ...
return err
}

回调的勾子函数:

  1. CREATE
Text
1
2
3
4
5
6
7
8
9
// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务
  1. UPDATE
Text
1
2
3
4
5
6
7
8
9
// 开始事务
BeforeSave
BeforeUpdate
// 关联前的 save
// 更新 db
// 关联后的 save
AfterUpdate
AfterSave
// 提交或回滚事务
  1. RETRIEVE
1
2
3
// 从 db 中加载数据
// Preloading (eager loading)
AfterFind
  1. DELETE
Text
1
2
3
4
5
// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务

跳过相应的钩子函数,只需要给当前执行的Session传递跳过勾子即可

1
2
3
4
//Db为前面获取的连接
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

相关查询资料:

  1. gorm查询嵌套结构体,嵌套预加载preload,关联,外键foreignkey,引用references - 腾讯云开发者社区-腾讯云 (tencent.com)

3.6、见 4.1

四、高级查询

4.1、UpSert

  1. 在冲突时,什么都不做
1
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
  1. 在`id`冲突时,将列更新为默认值 使用Map进行更新
1
2
3
4
5
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL
  1. 执行SQL语句
1
2
3
4
5
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));
  1. 在`id`冲突时,将列更新为新值
1
2
3
4
5
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL
  1. 除了主键全部进行更新
1
2
3
4
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;

BaiYZ 2022/6/28 下午4:18:09

Variables declared without an explicit initial value are given their zero value.

The zero value is:

  • 0 for numeric types,
  • false for the boolean type, and
  • "" (the empty string) for strings.