Gorm 使用指南

对象关系映射 Object Relational Mapping

对应关系:数据表=结构体 数据行=结构体实例 字段=结构体字段

优点:提高开发效率

缺点:牺牲执行性能 牺牲灵活性 弱化SQL能力

快速入门

安装

1
2
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

使用示例

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
package main

import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)

// 结构体定义
type Product struct {
gorm.Model
Code string
Price uint
}

func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}

// 迁移 schema
db.AutoMigrate(&Product{})

// Create
db.Create(&Product{Code: "D42", Price: 100})

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

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

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

声明模型

模型定义

模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成

1
2
3
4
5
6
7
8
type User struct {
ID uint
Name string
Age uint8
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}

约定

GORM 倾向于约定,而不是配置。默认情况下,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"`
}

高级选项

字段级权限控制

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

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
}

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

GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间。

如果您想要保存 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"` // 使用时间戳秒数填充创建时间
}

嵌入结构体

对于匿名字段,GORM 会将其字段包含在父结构体中。

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

对于正常的结构体字段,你也可以通过标签 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
}

并且,您可以使用标签 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
}

字段标签

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

标签名 说明
column 指定数据库列名
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 字段,它将跟踪 unix 秒,使用值nano/milli 跟踪 unix nano/milli 秒,例如:autoCreateTime:nano
autoUpdateTime 创建/更新时跟踪当前时间,对于 int 字段,它将跟踪 unix 秒,使用值nano/milli 跟踪 unix nano/milli 秒,例如:autoUpdateTime:milli
index 使用选项创建索引,为多个字段使用相同的名称创建复合索引
uniqueIndex 与索引相同,但创建唯一索引
check 创建检查约束,例如:check:age > 13
<- 设置字段的写权限, <-:create 仅创建字段, <-:update仅更新字段, <-:false 无写权限, <- 创建和更新权限
-> 设置字段的读取权限,->:false 没有读取权限
- 忽略该字段,- 没有读/写权限,-:migration 没有迁移权限,-:all 没有读/写/迁移权限
comment 迁移时为字段添加注释

连接到数据库

GORM 官方支持的数据库类型有: MySQL,PostgreSQL,SQlite,SQL Server

MySQL

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{})
}

MySQL 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:

1
2
3
4
5
6
7
8
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&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{})

自定义驱动

GORM 允许通过 DriverName 选项自定义 MySQL 驱动,例如:

1
2
3
4
5
6
7
8
9
10
import (
_ "example.com/my_mysql_driver"
"gorm.io/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, refer https://github.com/go-sql-driver/mysql#dsn-data-source-name
}), &gorm.Config{})

现有的数据库连接

GORM 允许通过一个现有的数据库连接来初始化 *gorm.DB

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{})

PostgreSQL

1
2
3
4
5
6
7
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

我们使用 pgx 作为 postgres 的 database/sql 驱动,默认情况下,它会启用 prepared statement 缓存,你可以这样禁用它:

1
2
3
4
5
// https://github.com/go-gorm/postgres
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: "user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai",
PreferSimpleProtocol: true, // disables implicit prepared statement usage
}), &gorm.Config{})

自定义驱动

GORM 允许通过 DriverName 选项自定义 PostgreSQL 驱动,例如:

1
2
3
4
5
6
7
8
9
import (
_ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
"gorm.io/gorm"
)

db, err := gorm.Open(postgres.New(postgres.Config{
DriverName: "cloudsqlpostgres",
DSN: "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable",
})

现有的数据库连接

GORM 允许通过一个现有的数据库连接来初始化 *gorm.DB

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

sqlDB, err := sql.Open("pgx", "mydb_dsn")
gormDB, err := gorm.Open(postgres.New(postgres.Config{
Conn: sqlDB,
}), &gorm.Config{})

连接池

GORM 使用 database/sql 维护连接池

1
2
3
4
5
6
7
8
9
10
sqlDB, err := db.DB()

// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)

// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)

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

CRUD接口

创建/Create

创建记录

1
2
3
4
5
6
7
user := User{Name: "Lare", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创建

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

用指定的字段创建记录

创建记录并更新给出的字段。

1
db.Select("Name", "Age", "CreatedAt").Create(&user)

创建一个记录且一同忽略传递给略去的字段值。

1
db.Omit("Name", "Age", "CreatedAt").Create(&user)

批量插入

要有效地插入大量记录,请将一个 slice 传递给 Create 方法。 GORM 将生成单独一条SQL语句来插入所有数据,并回填主键的值,钩子方法也会被调用。

1
2
3
4
5
6
var users = []User{{Name: "Lare1"}, {Name: "Lare2"}, {Name: "Lare3"}}
db.Create(&users)

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

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

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

// 数量为 100
db.CreateInBatches(users, 100)

创建钩子

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

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

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

如果您想跳过 钩子 方法,您可以使用 SkipHooks 会话模式

1
2
3
4
5
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)

根据 Map 创建

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

1
2
3
4
5
6
7
8
9
db.Model(&User{}).Create(map[string]interface{}{
"Name": "Lare", "Age": 18,
})

// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "Lare_1", "Age": 18},
{"Name": "Lare_2", "Age": 20},
})

使用 SQL 表达式、Context Valuer 创建记录

GORM 允许使用 SQL 表达式插入数据,有两种方法实现这个目标。根据 map[string]interface{} 或 自定义数据类型 创建,例如:

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
// 通过 map 创建记录
db.Model(User{}).Create(map[string]interface{}{
"Name": "Lare",
"Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("Lare",ST_PointFromText("POINT(100 100)"));

// 通过自定义类型创建记录
type Location struct {
X, Y int
}

// Scan 方法实现了 sql.Scanner 接口
func (loc *Location) Scan(v interface{}) error {
// Scan a value into struct from database driver
}

func (loc Location) GormDataType() string {
return "geometry"
}

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return clause.Expr{
SQL: "ST_PointFromText(?)",
Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
}
}

type User struct {
Name string
Location Location
}

db.Create(&User{
Name: "Lare",
Location: Location{X: 100, Y: 100},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("Lare",ST_PointFromText("POINT(100 100)"))

高级选项

关联创建

创建关联数据时,如果关联值是非零值,这些关联会被 upsert,且它们的 Hook 方法也会被调用

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: "Lare",
CreditCard: CreditCard{Number: "411111111111"}
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...

您也可以通过 SelectOmit 跳过关联保存,例如:

1
2
3
4
db.Omit("CreditCard").Create(&user)

// 跳过所有关联
db.Omit(clause.Associations).Create(&user)
默认值

您可以通过标签 default 为字段定义默认值,如:

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

插入记录到数据库时,默认值 会被用于 填充值为 零值 的字段

Upsert 及冲突

GORM 为不同数据库提供了兼容的 Upsert 支持

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
import "gorm.io/gorm/clause"

// 在冲突时,什么都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// 在`id`冲突时,将列更新为默认值
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

// 使用SQL语句
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));

// 在`id`冲突时,将列更新为新值
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL

// 在冲突时,更新除主键以外的所有列到新值。
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;

查询/Retrieve

检索单个对象

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取第一条记录(主键升序)
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)

FirstLast 会根据主键排序,分别查询第一条和最后一条记录。 只有在目标 struct 是指针或者通过 db.Model() 指定 model 时,该方法才有效。 此外,如果相关 model 没有定义主键,那么将按 model 的第一个字段进行排序。 例如:

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
var user User
var users []User

// 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

// doesn't work
result := map[string]interface{}{}
db.Table("users").First(&result)

// works with Take
result := map[string]interface{}{}
db.Table("users").Take(&result)

// 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
用主键检索

如果主键是数字类型,您可以使用 内联条件 来检索对象。 传入字符串参数时,需要特别注意 SQL 注入问题,查看 安全 获取详情。

1
2
3
4
5
6
7
8
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

当目标对象有主值时,主键将用于构建条件,例如:

1
2
3
4
5
6
7
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;

检索全部对象

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

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

条件

String 条件
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
// Get first matched record
db.Where("name = ?", "Lare").First(&user)
// SELECT * FROM users WHERE name = 'Lare' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "Lare").Find(&users)
// SELECT * FROM users WHERE name <> 'Lare';

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

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

// AND
db.Where("name = ? AND age >= ?", "Lare", "22").Find(&users)
// SELECT * FROM users WHERE name = 'Lare' 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';
Struct & Map 条件
1
2
3
4
5
6
7
8
9
10
11
// Struct
db.Where(&User{Name: "Lare", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "Lare" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "Lare", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "Lare" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
1
2
db.Where(&User{Name: "Lare", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "Lare";

要在查询条件中包含零值,您可以使用映射,它将包含所有键值作为查询条件,例如:

1
2
db.Where(map[string]interface{}{"Name": "Lare", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "Lare" AND age = 0;
指定结构体查询字段

使用 struct 进行搜索时,您可以通过将相关字段名称或 dbname 传递给 Where() 来指定要在查询条件中使用的 struct 中的哪些特定值,例如:

1
2
3
4
5
db.Where(&User{Name: "Lare"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "Lare" AND age = 0;

db.Where(&User{Name: "Lare"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;
内联条件

查询条件可以以与 Where 类似的方式内联到 First Find 等方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Get by primary key if it were a non-integer type
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "Lare")
// SELECT * FROM users WHERE name = "Lare";

db.Find(&users, "name <> ? AND age > ?", "Lare", 20)
// SELECT * FROM users WHERE name <> "Lare" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;
Not 条件

构建 NOT 条件,工作方式类似于 Where

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.Not("name = ?", "Lare").First(&user)
// SELECT * FROM users WHERE NOT name = "Lare" ORDER BY id LIMIT 1;

// Not In
db.Not(map[string]interface{}{"name": []string{"Lare", "Lare 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("Lare", "Lare 2");

// Struct
db.Not(User{Name: "Lare", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "Lare" AND age <> 18 ORDER BY id LIMIT 1;

// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
Or 条件
1
2
3
4
5
6
7
8
9
10
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'Lare'").Or(User{Name: "Lare 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'Lare' OR (name = 'Lare 2' AND age = 18);

// Map
db.Where("name = 'Lare'").Or(map[string]interface{}{"name": "Lare 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'Lare' OR (name = 'Lare 2' AND age = 18);

选择特定字段

Select 允许您指定要从数据库中检索的字段。 否则,GORM 将默认选择所有字段。

1
2
3
4
5
6
7
8
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;

db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;

db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;

Order

从数据库中检索记录时指定顺序。

1
2
3
4
5
6
7
8
9
10
11
db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

db.Clauses(clause.OrderBy{
Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)

Limit & Offset

Limit 指定要检索的最大记录数 Offset 指定在开始返回记录之前要跳过的记录数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)

Group By & Having

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
type result struct {
Date time.Time
Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
defer rows.Close()
for rows.Next() {
...
}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
defer rows.Close()
for rows.Next() {
...
}

type Result struct {
Date time.Time
Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("s

Distinct

从模型中选择不同的值

1
db.Distinct("name", "age").Order("name, age desc").Find(&results)

Joins

指定连接条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type result struct {
Name string
Email string
}

db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
...
}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "Lare@example.org").Joins(
Joins 预加载

您可以将 Joins 急切加载关联与单个 SQL 一起使用,例如:

1
2
db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

有条件加入

1
2
db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS
Joins a Derived Table

您还可以使用联接来联接派生表。

Scan

将结果扫描到结构中的工作方式类似于我们使用 Find 的方式

1
2
3
4
5
6
7
8
9
10
type Result struct {
Name string
Age int
}

var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)

// Raw SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)

更新/Update

保存所有字段

Save 会保存所有的字段,即使字段是零值。

1
2
3
4
5
6
db.First(&user)

user.Name = "Lare 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='Lare 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

更新单个列

当使用 Update 更新单个列时,你需要指定条件,否则会返回 ErrMissingWhereClause 错误。

1
2
3
4
5
6
7
8
9
10
11
// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// User 的 ID 是 `111`
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

更新多列

Updates 方法支持 structmap[string]interface{} 参数。当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段。

1
2
3
4
5
6
7
// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

更新选定字段

如果您想要在更新时选定、忽略某些字段,您可以使用 SelectOmit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// 使用 Struct 进行 Select(会 select 零值的字段)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "Lare", Role: "admin", Age: 0})

// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "Lare", Role: "admin", Age: 0})

更新 Hook

对于更新操作,GORM 支持 BeforeSaveBeforeUpdateAfterSaveAfterUpdate 钩子,这些方法将在更新记录时被调用。

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

批量更新

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

1
2
3
4
5
6
7
// 根据 struct 更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';

// 根据 map 更新
db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
阻止全局更新

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

对此,你必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate 模式,例如:

1
2
3
4
5
6
7
8
9
10
db.Model(&User{}).Update("name", "Lare").Error // gorm.ErrMissingWhereClause

db.Model(&User{}).Where("1 = 1").Update("name", "Lare")
// UPDATE users SET `name` = "Lare" WHERE 1=1

db.Exec("UPDATE users SET name = ?", "Lare")
// UPDATE users SET name = "Lare"

db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "Lare")
// UPDATE users SET `name` = "Lare"
更新的记录数

获取受更新影响的行数

1
2
3
4
5
6
// 通过 `RowsAffected` 得到更新的记录数
result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';

result.RowsAffected // 更新的记录数
result.Error // 更新的错误

删除/Delete

删除一条记录

删除一条记录时,删除对象需要指定主键,否则会触发 批量 Delete,例如:

1
2
3
4
5
6
7
// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "Lare").Delete(&email)
// DELETE from emails where id = 10 AND name = "Lare";

根据主键删除

GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字

1
2
3
4
5
6
7
8
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);

Delete Hook

对于删除操作,GORM 支持 BeforeDelete、AfterDelete Hook,在删除记录时会调用这些方法,查看 Hook 获取详情

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

批量删除

如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录

1
2
3
4
5
db.Where("email LIKE ?", "%Lare%").Delete(&Email{})
// DELETE from emails where email LIKE "%Lare%";

db.Delete(&Email{}, "email LIKE ?", "%Lare%")
// DELETE from emails where email LIKE "%Lare%";
阻止全局删除

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

对此,你必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate 模式,例如:

1
2
3
4
5
6
7
8
9
10
db.Delete(&User{}).Error // gorm.ErrMissingWhereClause

db.Where("1 = 1").Delete(&User{})
// DELETE FROM `users` WHERE 1=1

db.Exec("DELETE FROM users")
// DELETE FROM users

db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
// DELETE FROM users
返回删除行的数据

返回被删除的数据,仅适用于支持 Returning 的数据库,例如:

1
2
3
4
5
6
7
8
9
10
// 返回所有列
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "Lare", Role: "admin", Salary: 100}, {ID: 2, Name: "Lare.2", Role: "admin", Salary: 1000}}

// 返回指定的列
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "Lare", Role: "", Salary: 100}, {ID: 0, Name: "Lare.2", Role: "", Salary: 1000}}

软删除

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

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

1
2
3
4
5
6
7
8
9
10
11
// user 的 ID 是 `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// 批量删除
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// 在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
查找被软删除的记录

您可以使用 Unscoped 找到被软删除的记录

1
2
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
永久删除

您也可以使用 Unscoped 永久删除匹配的记录

1
2
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;

错误处理

GORM 的错误处理与常见的 Go 代码不同,因为 GORM 提供的是链式 API。

如果遇到任何错误,GORM 会设置 *gorm.DBError 字段,您需要像这样检查它:

1
2
3
if err := db.Where("name = ?", "Lare").First(&user).Error; err != nil {
// 处理错误...
}

或者

1
2
3
if result := db.Where("name = ?", "Lare").First(&user); result.Error != nil {
// 处理错误...
}

ErrRecordNotFound

FirstLastTake 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误。如果发生了多个错误,你可以通过 errors.Is 判断错误是否为 ErrRecordNotFound,例如:

1
2
3
// 检查错误是否为 RecordNotFound
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)

错误列表

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
package gorm

import (
"errors"

"gorm.io/gorm/logger"
)

var (
// ErrRecordNotFound 记录未找到错误
ErrRecordNotFound = logger.ErrRecordNotFound
// ErrInvalidTransaction 当您尝试“提交”或“回滚”时,交易无效
ErrInvalidTransaction = errors.New("invalid transaction")
// ErrNotImplemented 未实现
ErrNotImplemented = errors.New("not implemented")
// ErrMissingWhereClause 缺少 where 子句
ErrMissingWhereClause = errors.New("WHERE conditions required")
// ErrUnsupportedRelation 不受支持的关系
ErrUnsupportedRelation = errors.New("unsupported relations")
// ErrPrimaryKeyRequired 必需的主键
ErrPrimaryKeyRequired = errors.New("primary key required")
// ErrModelValueRequired 必需的模型值
ErrModelValueRequired = errors.New("model value required")
// ErrInvalidData 不支持的数据
ErrInvalidData = errors.New("unsupported data")
// ErrUnsupportedDriver 不支持的驱动程序
ErrUnsupportedDriver = errors.New("unsupported driver")
// ErrRegistered 已注册
ErrRegistered = errors.New("registered")
// ErrInvalidField 无效字段
ErrInvalidField = errors.New("invalid field")
// ErrEmptySlice 发现空切片
ErrEmptySlice = errors.New("empty slice found")
// ErrDryRunModeUnsupported 不支持空运行模式
ErrDryRunModeUnsupported = errors.New("dry run mode unsupported")
// ErrInvalidDB 无效数据库
ErrInvalidDB = errors.New("invalid db")
// ErrInvalidValue 无效值
ErrInvalidValue = errors.New("invalid value, should be pointer to struct or slice")
// ErrInvalidValueOfLength 无效值与长度不匹配
ErrInvalidValueOfLength = errors.New("invalid association values, length doesn't match")
// ErrPreloadNotAllowed 使用计数时不允许预加载
ErrPreloadNotAllowed = errors.New("preload is not allowed when count is used")
)

链式操作

GORM 允许进行链式操作,所以您可以像这样写代码:

1
db.Where("name = ?", "Lare").Where("age = ?", 18).First(&user)

GORM 中有三种类型的方法: 链式方法Finisher 方法新建会话方法

链式方法是修改或添加子句到当前语句的方法,如:

Where、Select、Omit、Joins、Scopes、Preload、Raw(Raw 不能与其他可链接的方法一起使用来构建 SQL)……

迁移

AutoMigrate

AutoMigrate 用于自动迁移您的 schema,保持您的 schema 是最新的。

1
2
3
4
5
6
db.AutoMigrate(&User{})

db.AutoMigrate(&User{}, &Product{}, &Order{})

// 创建表时添加后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
1
2
3
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})

Migrator 接口

GORM 提供了 Migrator 接口,该接口为每个数据库提供了统一的 API 接口,可用来为您的数据库构建独立迁移,例如:

SQLite 不支持 ALTER COLUMNDROP COLUMN,当你试图修改表结构,GORM 将创建一个新表、复制所有数据、删除旧表、重命名新表。

一些版本的 MySQL 不支持 rename 列,索引。GORM 将基于您使用 MySQL 的版本执行不同 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
type Migrator interface {
// AutoMigrate
AutoMigrate(dst ...interface{}) error

// Database
CurrentDatabase() string
FullDataTypeOf(*schema.Field) clause.Expr

// Tables
CreateTable(dst ...interface{}) error
DropTable(dst ...interface{}) error
HasTable(dst interface{}) bool
RenameTable(oldName, newName interface{}) error
GetTables() (tableList []string, err error)

// Columns
AddColumn(dst interface{}, field string) error
DropColumn(dst interface{}, field string) error
AlterColumn(dst interface{}, field string) error
MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
HasColumn(dst interface{}, field string) bool
RenameColumn(dst interface{}, oldName, field string) error
ColumnTypes(dst interface{}) ([]ColumnType, error)

// Constraints
CreateConstraint(dst interface{}, name string) error
DropConstraint(dst interface{}, name string) error
HasConstraint(dst interface{}, name string) bool

// Indexes
CreateIndex(dst interface{}, name string) error
DropIndex(dst interface{}, name string) error
HasIndex(dst interface{}, name string) bool
RenameIndex(dst interface{}, oldName, newName string) error
}

当前数据库

1
2
// 当前使用的数据库名
db.Migrator().CurrentDatabase()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 为 `User` 创建表
db.Migrator().CreateTable(&User{})

// 将 "ENGINE=InnoDB" 添加到创建 `User` 的 SQL 里去
db.Set("gorm:table_options", "ENGINE=InnoDB").Migrator().CreateTable(&User{})

// 检查 `User` 对应的表是否存在
db.Migrator().HasTable(&User{})
db.Migrator().HasTable("users")

// 如果存在表则删除(删除时会忽略、删除外键约束)
db.Migrator().DropTable(&User{})
db.Migrator().DropTable("users")

// 重命名表
db.Migrator().RenameTable(&User{}, &UserInfo{})
db.Migrator().RenameTable("users", "user_infos")

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
type User struct {
Name string
}

// Add name field
db.Migrator().AddColumn(&User{}, "Name")
// Drop name field
db.Migrator().DropColumn(&User{}, "Name")
// Alter name field
db.Migrator().AlterColumn(&User{}, "Name")
// Check column exists
db.Migrator().HasColumn(&User{}, "Name")

type User struct {
Name string
NewName string
}

// Rename column to new name
db.Migrator().RenameColumn(&User{}, "Name", "NewName")
db.Migrator().RenameColumn(&User{}, "name", "new_name")

// ColumnTypes
db.Migrator().ColumnTypes(&User{}) ([]gorm.ColumnType, error)

type ColumnType interface {
Name() string
DatabaseTypeName() string // varchar
ColumnType() (columnType string, ok bool) // varchar(64)
PrimaryKey() (isPrimaryKey bool, ok bool)
AutoIncrement() (isAutoIncrement bool, ok bool)
Length() (length int64, ok bool)
DecimalSize() (precision int64, scale int64, ok bool)
Nullable() (nullable bool, ok bool)
Unique() (unique bool, ok bool)
ScanType() reflect.Type
Comment() (value string, ok bool)
DefaultValue() (value string, ok bool)
}

约束

1
2
3
4
5
6
7
8
9
10
11
12
type UserIndex struct {
Name string `gorm:"check:name_checker,name <> 'Lare'"`
}

// 创建约束
db.Migrator().CreateConstraint(&User{}, "name_checker")

// 删除约束
db.Migrator().DropConstraint(&User{}, "name_checker")

// 检查约束是否存在
db.Migrator().HasConstraint(&User{}, "name_checker")

索引

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
type User struct {
gorm.Model
Name string `gorm:"size:255;index:idx_name,unique"`
}

// 为 Name 字段创建索引
db.Migrator().CreateIndex(&User{}, "Name")
db.Migrator().CreateIndex(&User{}, "idx_name")

// 为 Name 字段删除索引
db.Migrator().DropIndex(&User{}, "Name")
db.Migrator().DropIndex(&User{}, "idx_name")

// 检查索引是否存在
db.Migrator().HasIndex(&User{}, "Name")
db.Migrator().HasIndex(&User{}, "idx_name")

type User struct {
gorm.Model
Name string `gorm:"size:255;index:idx_name,unique"`
Name2 string `gorm:"size:255;index:idx_name_2,unique"`
}
// 修改索引名
db.Migrator().RenameIndex(&User{}, "Name", "Name2")
db.Migrator().RenameIndex(&User{}, "idx_name", "idx_name_2")

日志

Gorm 有一个 默认 logger 实现,默认情况下,它会打印慢 SQL 和错误

Logger 接受的选项不多,您可以在初始化时自定义它,例如:

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

// 全局模式
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})

// 新建会话模式
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)

日志级别

GORM 定义了这些日志级别:SilentErrorWarnInfo

1
2
3
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})

自定义 Logger

1
2
3
4
5
6
7
type Interface interface {
LogMode(LogLevel) Interface
Info(context.Context, string, ...interface{})
Warn(context.Context, string, ...interface{})
Error(context.Context, string, ...interface{})
Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}

安全

GORM 使用 database/sql 的参数占位符来构造 SQL 语句,这可以自动转义参数,避免 SQL 注入数据

查询条件

1
2
3
4
5
6
7
userInput := "jinzhu;drop table users;"

// 安全的,会被转义
db.Where("name = ?", userInput).First(&user)

// SQL 注入
db.Where(fmt.Sprintf("name = %v", userInput)).First(&user)

内联条件

1
2
3
4
5
// 会被转义
db.First(&user, "name = ?", userInput)

// SQL 注入
db.First(&user, fmt.Sprintf("name = %v", userInput))

当通过用户输入的整形主键检索记录时,你应该对变量进行类型检查。

1
2
3
4
5
6
7
8
9
10
11
userInputID := "1=1;drop table users;"
// 安全的,返回 err
id,err := strconv.Atoi(userInputID)
if err != nil {
return error
}
db.First(&user, id)

// SQL 注入
db.First(&user, userInputID)
// SELECT * FROM users WHERE 1=1;drop table users;

SQL 注入方法

为了支持某些功能,一些输入不会被转义,调用方法时要小心用户输入的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.Select("name; drop table users;").First(&user)
db.Distinct("name; drop table users;").First(&user)

db.Model(&user).Pluck("name; drop table users;", &names)

db.Group("name; drop table users;").First(&user)

db.Group("name").Having("1 = 1;drop table users;").First(&user)

db.Raw("select name from users; drop table users;").First(&user)

db.Exec("select name from users; drop table users;")

db.Order("name; drop table users;").First(&user)