1. 简介
github仓库地址:https://github.com/go-gorm/gorm
文档地址:https://pkg.go.dev/gorm.io/gorm
GORM 是一个 Go 语言下的 ORM 库。ORM 全称 Object Relational Mapping(对象关系映射),用于实现面向对象编程语言里数据之间的转换,应用程序通过它可以连接关系型数据库并读写数据。GORM 支持的数据库类型包括: MySQL、PostreSQL、SQLite、SQL Server、TiDB。
1.1 声明模型
GORM 通过将 Go 结构体映射到数据库表来简化数据库交互,模型使用结构体定义,可以包含基本类型、指针类型、类型别名,也可以是实现了 database/sql 包中的 Scanner 和 Valuer 接口的类型。
以下示例模型中,包含基本类型 uint、string、uint8,也包含指针类型表示可空字段,sql.NullString 和 sql.NullTime 用于更多控制的可空字段,CreatedAt 和 UpdatedAt 是特殊字段,记录被创建或更新时自动填充。
type User struct {
ID uint // Standard field for the primary key
Name string // 一个常规字符串字段
Email *string // 一个指向字符串的指针, allowing for null values
Age uint8 // 一个未签名的8位整数
Birthday *time.Time // A pointer to time.Time, can be null
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields
CreatedAt time.Time // 创建时间(由GORM自动管理)
UpdatedAt time.Time // 最后一次更新时间(由GORM自动管理)
}
GORM 有如下默认的约定,遵循这些约定可以简化代码编写,也可以通过代码自定义这些设置。
- 将结构体名称转换为 snake_case 并加上复数形式作为表名,如 UserInfo 结构体默认表名为 user_infos
- 将字段名称转换为 snake_case 作为列名
- 使用一个名为 ID 的字段作为默认主键
- 使用 CreatedAt 和 UpdatedAt 自动跟踪记录的创建和更新时间
GORM 还提供了一个预定义结构体 gorm.Model,可以将其嵌入结构体以自动包含这些字段。
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
字段标签
结构体成员的标签大小写不敏感,但建议使用 camelCase 风格。多个标签用 ; 分隔。
如下标签指定了成员对应的列名,设置只允许读。
Name string `gorm:"column:name;->"`
如下为 GORM 支持的标签。
标签名 | 说明 |
---|---|
column | 列名 |
type | 列数据类型 |
serializer | 指定将数据序列化或反序列化到数据库中的序列化器 |
size | 列数据类型的大小或长度 |
primaryKey | 将列定义为主键 |
unique | 将列定义为唯一键 |
default | 定义列的默认值 |
precision | 指定列的精度 |
scale | 指定列大小 |
not null | 指定列为 NOT NULL |
embedded | 嵌套字段 |
embeddedPrefix | 嵌入字段的列名前缀 |
autoCreateTime | 创建时追踪当前时间 |
autoUpdateTime | 创建/更新时追踪当前时间 |
index | 根据参数创建索引 |
uniqueIndex | 与 index 相同,但创建的是唯一索引 |
check | 创建检查约束,例如 check:age > 13 |
<- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 |
-> | 设置字段读的权限,->:false 无读权限 |
- | 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 |
comment | 迁移时为字段添加注释 |
字段权限控制
可导出字段在使用 GORM 进行 CRUD 时拥有全部权限,可以使用标签来控制某个字段的权限,包含只读、只写、只创建、只更新或者被忽略。
type User struct {
Name string `gorm:"<-:create"` // 允许读和创建
Name string `gorm:"<-:update"` // 允许读和更新
Name string `gorm:"<-"` // 允许读和写(创建和更新)
Name string `gorm:"<-:false"` // 允许读,禁止写
Name string `gorm:"->"` // 只读(除非有自定义配置,否则禁止写)
Name string `gorm:"->;<-:create"` // 允许读和写
Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
Name string `gorm:"-"` // 通过 struct 读写会忽略该字段
Name string `gorm:"-:all"` // 通过 struct 读写、迁移会忽略该字段
Name string `gorm:"-:migration"` // 通过 struct 迁移会忽略该字段
}
创建/更新时间追踪
GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间,要使用不同名称的字段,可以配置 autoCreateTime、autoUpdateTime 标签。
字段类型表示时间的精度。
type User struct {
CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
UpdatedAt int // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
Updated int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳纳秒数填充更新时间
Updated int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
Created int64 `gorm:"autoCreateTime"` // 使用时间戳秒数填充创建时间
}
2. 使用
2.1 安装
使用 go get 将 GORM 包下载到 GOPATH 指定的目录下。
go get -u gorm.io/gorm
2.2 连接数据库
以连接 MySQL 为例,在变量 dsn 中指定 IP 和端口、用户名和密码、数据库名称、连接参数,这里使用了默认配置。
dsn 全称 Data Source Name,也就是数据库名称。
dsn := "user:passwd@tcp(ip:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
可以在连接时指定一些配置。
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.DB。
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
}), &gorm.Config{})
GORM 使用 database/sql 来维护连接池。
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10) // 设置最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 设置最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 设置最大连接时间
2.3 创建
Create 方法向表中插入一条记录,参数传入结构体的指针。
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
Create 方法还支持插入多条记录,参数传入结构体或结构体指针的切片。
users := []User{
{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(users) // 传入切片
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
CreateInBatches 方法可以在插入多条记录时按照批次处理,并指定批次大小,防止一次插入太多数据而失败。
db.CreateInBatches(users, 100)
Create 方法支持通过 map[string]interface{} 与 []map[string]interface{}{}来创建记录。
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})
指定和忽略对应字段
创建记录并为指定字段赋值。
db.Select("Name", "Age").Create(&user)
创建记录并忽略指定字段。
db.Omit("Age", "CreatedAt").Create(&user)
插入时更新
在插入产生冲突时,可以指定接下来执行的动作。
import "gorm.io/gorm/clause"
// 发生冲突时不执行
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// 某字段发生冲突时,设置另外的字段为某个值
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 ***;
// 某字段发生冲突时,设置另外的字段为 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));
// 某字段发生冲突时,用结构体对象成员更新指定字段的值
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);
// 发生冲突时更新所有值
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...;
2.4 查询
查询单个对象
通过 First、Take、Last 方法查询一个对象返回,如果没有找到记录会返回 ErrRecordNotFound 错误。
// 获取第一条记录(主键升序)
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 // 返回错误
// 检查是否不存在记录的错误
if errors.Is(result.Error, gorm.ErrRecordNotFound) {}
如果不想产生 ErrRecordNotFound错误,可以使用 Find 方法,可以接受 struct 和 slice 的数据。如果不带 limit 则会查询整个表并返回第一个对象,性能不高且不确定。
db.Limit(1).Find(&user)
根据主键检索
查询方法可以带有额外参数来按主键检索。
对于数字类型主键:
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);
对于字符串类型主键:
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";
当目标对象有一个主键值时,将使用主键构建查询条件。
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;
查询多个对象
通过 Find 和 Scan 方法查询多个对象。它们的用法类似,区别在于 Find 支持更多的回调函数,Scan 使用时需要指定表名。
不带条件将会查询所有记录,通常加上一些条件来查询。
// 查询所有记录
result := db.Find(&users)
// SELECT * FROM users;
// 根据条件查询记录
var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)
条件
通过字符串表示筛选条件。
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// 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';
如果对象设置了主键,条件查询又筛选了主键,将会将筛选加起来,有查不到结果的风险。
var user = User{ID: 10}
db.Where("id = ?", 20).First(&user)
// SELECT * FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1;
通过 struct 和 map 表示筛选条件。
当用 struct 来筛选条件时,除非指定字段,否则零值的成员将会被忽略,不会被用来查询,如 Age: 0 不会被用来查询 age = 0。
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Struct 指定字段
db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
// Struct 指定字段
db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;
// Struct 指定查询条件
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
Not 方法表示 not 条件。
db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;
// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" 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 方法表示 or 条件。
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
选择字段
Select 方法能指定 select 的字段,不指定默认选择所有字段。
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;
排序
Order 方法进行排序。
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;
分页
Limit 和 Offset 方法指定分页大小。
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;
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 limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)
// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
聚合
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"
Distinct
db.Distinct("name", "age").Order("name, age desc").Find(&results)
表连接
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`;
// inner join
db.InnerJoins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` INNER JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;
记数
// 计数
var count int64
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu'
// 为不同 name 计数
db.Model(&User{}).Distinct("name").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`
// 按名称分组后计数
db.Model(&User{}).Group("name").Count(&count)
// SELECT COUNT(1) FROM `users` GROUP BY `name`
子查询
// 简单的子查询
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// SQL: SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");
// 内嵌子查询
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
// SQL: SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")
// 在 FROM 子句中使用子查询
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{})
// SQL: SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18
2.5 更新
更新所有列
Save 方法保存所有字段,即使字段是零值。如果保存值不包含主键,将执行 Create,否则 Update 所有字段。
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
更新指定列
Update 方法更新单列,需要指定一些条件,否则会引起 ErrMissingWhereClause 错误。
Updates 方法更新多列,支持 struct 和 map 参数,当使用 struct 更新是默认只更新非零值字段。
在更新时可以通过 Select 和 Omit 方法选择、忽略某些字段。
// 更新单列
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;
// 根据 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;
使用表达式更新
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
db.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;
返回修改行数据
// 返回所有列
var users []User
db.Model(&users).Clauses(clause.Returning{}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING *
// 返回指定列
db.Model(&users).Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING `name`, `salary`
2.6 删除
删除一条记录
删除一条记录时,删除对象需要指定主键,否则会触发批量删除。
// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;
// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
根据主键删除
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);
批量删除
如果对象不包含主键的值,则只会根据匹配条件删除。
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";
db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";
如果主键设置了值,则还会判断主键的值。
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// DELETE FROM users WHERE id IN (1,2,3);
db.Delete(&users, "name LIKE ?", "%jinzhu%")
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3);
返回删除行的数据
// 回写所有的列
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// 回写指定的列
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`
2.7 原生SQL
Raw 和 Scan 方法可以直接写完整的 SQL 语句并执行。
type Result struct {
ID int
Name string
Age int
}
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)
var age int
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)
Exec 方法直接执行一个 SQL 语句。
db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
命名参数
GORM 支持不同形式的命名参数传入方式。
// sql.Named
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"
// map
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(&result3)
// SELECT * FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.`id` LIMIT 1
// struct
db.Raw("SELECT * FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2", NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"
只生成 SQL
GORM 使用 database/sql 的参数占位符来构建 SQL 语句,可以使用 ToSQL 语句只生成 SQL 而不执行。
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
获取返回结果
将结果返回为 *sql.Row 或 *sql.Rows,再通过 Scan 方法解析其中的信息,可以在解析的时候做其他的业务逻辑处理。
// 一行
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()
row.Scan(&name, &age)
// 多行
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()
defer rows.Close()
for rows.Next() {
// 将 row 扫描至成员变量
rows.Scan(&name, &age, &email)
// 将 row 扫描至 struct 变量
db.ScanRows(rows, &user)
// 业务逻辑...
}
3. 功能
3.1 上下文
GORM 的上下文支持由 WithContext 方法启用,可以增强 Go 应用程序中数据库操作的灵活性和控制力。 它允许在不同的操作模式、超时设置以及甚至集成到钩子/回调和中间件中进行上下文管理。
db.WithContext(ctx).Find(&users)
// 在多个操作中保留 context
tx := db.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")
通过 context 来设置超时时间。
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
db.WithContext(ctx).Find(&users)
可以在 hooks 或 callbacks 中使用 context 获取信息。
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
ctx := tx.Statement.Context
// ... use context
return
}
3.2 错误处理
GORM 将错误处理集成到其可链式方法语法中。 *gorm.DB 实例包含一个 Error 字段,当发生错误时会被设置。
通常的做法是在执行数据库操作后,特别是在完成方法后,检查这个字段。
if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
// 处理错误...
}
if result := db.Where("name = ?", "jinzhu").First(&user); result.Error != nil {
// 处理错误...
}
当使用 First、Last、Take 等方法未找到记录时,GORM 会返回 ErrRecordNotFound。
err := db.First(&user, 100).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// 处理未找到记录的错误...
}
解析数据库的特定代码错误。
import (
"github.com/go-sql-driver/mysql"
"gorm.io/gorm"
)
result := db.Create(&newRecord)
if result.Error != nil {
if mysqlErr, ok := result.Error.(*mysql.MySQLError); ok {
switch mysqlErr.Number {
case 1062: // MySQL中表示重复条目的代码
// 处理重复条目
// 为其他特定错误代码添加案例
default:
// 处理其他错误
}
} else {
// 处理非MySQL错误或未知错误
}
}
3.3 钩子
Hook(钩子)是在创建、查询、更新、删除等操作之前、之后调用的函数。如果为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。
钩子方法的函数签名应该是 func(*gorm.DB) error
,基于参数 *gorm.DB 的操作会在同一个事务中。
创建
创建时可用的 hook:
// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务
方法定义示例:
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if !u.IsValid() {
err = errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.ID == 1 {
tx.Model(u).Update("role", "admin")
}
return
}
更新
更新时可用的 hook:
// 开始事务
BeforeSave
BeforeUpdate
// 关联前的 save
// 更新 db
// 关联后的 save
AfterUpdate
AfterSave
// 提交或回滚事务
方法定义示例:
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
if u.readonly() {
err = errors.New("read only user")
}
return
}
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
if u.Confirmed {
tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
}
return
}
删除
删除时可用的 hook:
// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务
方法定义示例:
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
if u.Confirmed {
tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
}
return
}
查询
查询时可用的 hook:
// 从 db 中加载数据
// Preloading (eager loading)
AfterFind
方法定义示例:
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.MemberShip == "" {
u.MemberShip = "user"
}
return
}
3.4 事务
GORM 默认在事务中执行写入操作(创建、更新、删除)。
禁用事务
在初始化时禁用它将获得大约 30%+ 性能提升。
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
// 当前会话禁用
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
使用事务
定义事务函数 func(tx *gorm.DB) error
,然后通过 Transaction 方法调用它,函数体内执行一系列操作,将会以事务进行。
// 定义事务函数
txF := func(tx *gorm.DB) error {
if err := tx.Create(&User{Uid: 123, Name: "name1"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&User{Uid: 124, Name: "name2"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
}
// Transaction 方法调用事务
err := db.Transaction(txF)
// 直接在 Transaction 方法中定义事务函数
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
GORM 支持嵌套事务。
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback user2") // Rollback user2
})
tx.Transaction(func(tx3 *gorm.DB) error {
tx3.Create(&user3)
return nil
})
return nil // Commit user1, user3
})
手动事务
通过直接调用事务控制方法来执行事务。
// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作
tx.Create(...)
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()
保存点
可以设置保存点,并且回滚至保存点。
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
3.5 日志
默认 logger 实现
GORM 的默认 logger会打印慢 SQL 和错误。
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Silent, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: false, // Disable color
},
)
// globally mode
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})
// session mode
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)
日志级别有 Silent、Error、Warn、Info。
// 连接全局设置级别
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
// 单个操作设置级别
db.Debug().Where("name = ?", "jinzhu").First(&User{})
自定义 logger
可以自定义 logger,只需要实现以下接口:
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)
}