GORM 官方中文文档:https://gorm.io/zh_CN/docs/index.html
基础使用
安装 GORM
go get -u gorm.io/gorm
go get -u gorm.io/driver/xxx
xxx 表示需要适用的数据库驱动,支持 mysql
、postgres
、sqlite
、sqlserver
、clickhouse
连接数据库
最简单的数据库连接方式
// 账户:密码@(链接地址:端口)/库名? ... 更多参数
dsn := "root:root@(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
实际使用中会通过 Mmysql.New
的方式设置连接信息,这样可以额外控制一些高级配置:
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "root:root@(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{})
gorm.Config
Gorm 也可以在初始化数据库连接时设置一些配置:
type Config struct {
SkipDefaultTransaction bool // 跳过默认事务
NamingStrategy schema.Namer
Logger logger.Interface
NowFunc func() time.Time
DryRun bool
PrepareStmt bool
DisableNestedTransaction bool
AllowGlobalUpdate bool
DisableAutomaticPing bool
DisableForeignKeyConstraintWhenMigrating bool
}
gorm.Config 是针对 GROM 本身的设置选项
GORM 模型
使用 ORM 工具时,通常要在代码中定义模型 Models
与数据库中的数据表进行映射,在 GORM 中模型通常是定义的结构体、基本的 Go 类型、实现了 sql.Scanner
和 driver.Valuer
接口的自定义类型以及其指针或别名组成。
模型定义
为了方便定义模型, GORM 内置了一个 gorm.Model
结构体。这个结构体内置了四个字段:ID
、CreatedAt
、UpdatedAt
、DeletedAt
:
// gorm.Model 结构体
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
可以将这个模型嵌入自己的模型中,无需重复定义这些字段。
示例
type User struct {
gorm.Model
Name string
Age sql.NullInt64
Birthday *time.Time
Email string `gorm:"type:varchar(100);unique_index"`
Role string `gorm:"size:255"` // 设置字段大小为255
MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)唯一并且不为空
Num int `gorm:"AUTO_INCREMENT"` // 设置 num 为自增类型
Address string `gorm:"index:addr"` // 给address字段创建名为addr的索引
IgnoreMe int `gorm:"-"` // 忽略本字段
}
Model 标记
使用结构体声明模型时,标记是可选的,GORM 支持以下标记,标记名称大小写不敏感,但建议使用小驼峰 camelCase
。
结构体标记 Struct Tags
标签名 | 说明 |
---|---|
column | 指定 db 列名 |
type | 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null 、size , autoIncrement … 像 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 | 设置列大小,默认 255 |
primaryKey | 设置字段为主键 |
unique | 设置字段为唯一 |
default | 设置字段默认值 |
precision | 设置列精度 |
scale | 指定列大小 |
not null | 指定列为 NOT NULL |
autoIncrement | 指定列为自动增长 |
autoIncrementIncrement | 自增字段的自增步长 |
embedded | 嵌套字段 |
embeddedPrefix | 嵌入字段的列名前缀 |
autoCreateTime | 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano |
autoUpdateTime | 创建 / 更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli |
index | 根据参数创建索引,多个字段使用相同的名称则创建复合索引,具体使用方法参考: 索引 |
uniqueIndex | 创建唯一索引 |
check | 创建检查约束,例如 check:age > 13 ,查看 约束 获取详情 |
<- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 |
-> | 设置字段读的权限,->:false 无读权限 |
- | 忽略此字段, - no 无读写权限, -:migration 无迁移权限, -:all 无 读/写/迁移权限 |
comment | 迁移时为字段添加注释 |
关联标记
标签 | 描述 |
---|---|
foreignKey | 指定当前模型的列作为连接表的外键 |
references | 指定引用表的列名,其将被映射为连接表外键 |
polymorphic | 指定多态类型,比如模型名 |
polymorphicValue | 指定多态值、默认表名 |
many2many | 指定连接表表名 |
joinForeignKey | 指定连接表的外键列名,其将被映射到当前表 |
joinReferences | 指定连接表的外键列名,其将被映射到引用表 |
constraint | 关系约束,例如:OnUpdate 、OnDelete |
名称约定
主键 Primary
默认情况下,GORM 会使用 ID 作为表的主键。
type User struct {
ID string // 默认情况下,名为 `ID` 的字段会作为表的主键
Name string
}
可以通过 primaryKey
字段将其他字段设为主键。
// 将 `UUID` 设为主键
type Animal struct {
ID int64
UUID string `gorm:"primaryKey"`
Name string
Age int64
}
复合主键
通过把多个字段设置为主键,表示创建为复合主键:
type Product struct {
ID string `gorm:"primaryKey"`
LanguageCode string `gorm:"primaryKey"`
Code string
Name string
}
PrioritizedPrimaryField
启用了 AutoIncrement
,要禁用它,您需要为整型字段关闭 autoIncrement
:type Product struct {
CategoryID uint64 `gorm:"primaryKey;autoIncrement:false"`
TypeID uint64 `gorm:"primaryKey;autoIncrement:false"`
}
表名 Table Name
GORM 使用结构体名为大驼峰,默认表名称为结构体名称后面加复数,即 type User struct{}
的表名为 users
。
可以 db.SingularTable(true)
的方式禁用默认表名的复数形式。
TableName()
通过定义结构体的 TableName() 方法,修改默认表名。
type User struct {} // 默认表名是 `users`
// 将 User 的表名设置为 `profiles`
func (User) TableName() string {
return "profiles"
}
func UserTable(user User) func (tx *gorm.DB) *gorm.DB {
return func (tx *gorm.DB) *gorm.DB {
if user.Admin {
return tx.Table("admin_users")
}
return tx.Table("users")
}
}
db.Scopes(UserTable(user)).Create(&user)
临时指定表名
使用 Table()
方法临时指定表名,例如:
// 根据 User 的字段创建 `deleted_users` 表
db.Table("deleted_users").AutoMigrate(&User{})
// 从另一张表查询数据
var deletedUsers []User
db.Table("deleted_users").Find(&deletedUsers)
// SELECT * FROM deleted_users;
db.Table("deleted_users").Where("name = ?", "jinzhu").Delete(&User{})
// DELETE FROM deleted_users WHERE name = 'jinzhu';
命名策略:
GORM 允许用户通过覆盖默认的命名策略更改默认的命名约定,命名策略被用于构建: TableName、ColumnName、JoinTableName、RelationshipFKName、CheckerName、IndexName。查看 [GORM 配置](https://gorm.io/zh_CN/docs/gorm_config.html#naming_strategy) 获取详情.
列名 Column Name
列名命名规则为大驼峰,默认转换为数据库的字段名称为 下划线:
type User struct {
ID uint // 列名是 `id`
Name string // 列名是 `name`
Birthday time.Time // 列名是 `birthday`
CreatedAt time.Time // 列名是 `created_at`
}
您可以使用 column
标签 或者命名策略来覆盖列名:
type Animal struct {
AnimalID int64 `gorm:"column:beast_id"` // 将列名设为 `beast_id`
Birthday time.Time `gorm:"column:day_of_the_beast"` // 将列名设为 `day_of_the_beast`
Age int64 `gorm:"column:age_of_the_beast"` // 将列名设为 `age_of_the_beast`
}
时间戳跟踪
CreatedAt
如果有 CreatedAt
字段,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间。
db.Create(&user) // 将 `CreatedAt` 设为当前时间
user2 := User{Name: "jinzhu", CreatedAt: time.Now()}
db.Create(&user2) // user2 的 `CreatedAt` 不会被修改
// 想要修改该值,您可以使用 `Update`
db.Model(&user).Update("CreatedAt", time.Now())
也可以设置 autoCreateTime:false
来禁用掉时间戳跟踪
type User struct {
CreatedAt time.Time `gorm:"autoCreateTime:false"`
}
UpdatedAt
如果模型有 UpdatedAt
字段,该字段的值将会是每次更新记录的时间。
db.Save(&user) // `UpdatedAt`将会是当前时间
db.Model(&user).Update("name", "jinzhu") // `UpdatedAt`将会是当前时间
DeletedAt
如果模型有 DeletedAt
字段,调用 Delete
删除该记录时,将会设置 DeletedAt
字段为当前时间,而不是直接将记录从数据库中删除。
增删改查
按照GORM 官方例子,我们创建一个如下结构的 users
表:
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`name` varchar(100) NOT NULL COMMENT '用户名',
`age` tinyint(3) unsigned DEFAULT NULL COMMENT '年龄',
`birthday` datetime NOT NULL,
`member_number` varchar(255) DEFAULT NULL,
`activated_at` datetime DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
同时创建对应的结构体:
type User struct {
gorm.Model
Name string
Age uint8
Birthday time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
}
插入数据
插入单条
user := User{Name: "lisi", Age: 14, Birthday: time.Now()}
result := db.Create(&user)
// 如果插入失败 就输出错误日志
if result.Error != nil {
fmt.Println(result.Error)
}
fmt.Println(user.ID) // 插入数据的 id
fmt.Println(result.RowsAffected) // 插入记录的返回条数
指定字段
可以通过 Select
方法,指定插入结构体中的哪些字段:
user := User{Name: "lisi", Age: 14, Birthday: time.Now()}
// 只插入给出的字段:INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("lisi", 14, "2022-06-19 11:05:21")
result := db.Select("Name", "Age", "CreatedAt").Create(&user)
或者通过 Omit
方法排除掉需要忽略的字段:
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2022-06-19 11:05:21")
result := db.Omit("Name", "Age", "CreatedAt").Create(&user)
批量插入
如果需要批量插入大量数据,则直接将 slice 传递给 Create
方法即可。GORM 在执行SQL时会生成单独的一条 SQL 来插入所有数据。
如果有钩子方法,也会被调用。
var users = []User{{Name: "zhangsan"}, {Name: "lisi"}, {Name: "wangwu"}}
result := db.Create(&users)
// 如果插入失败 就输出错误日志
if result.Error != nil {
fmt.Println(result.Error)
}
for _, user := range users {
fmt.Printf("插入用户ID:%d,插入用户名:%s\n", user.ID, user.Name)
}
如果数据量巨大,需要分批插入则可以调用 CreateInBatches
方法,此方法会按照指定的数量进行拆分拆入:
var users = []User{{Name: "zhangsan"}, {Name: "lisi"}, {Name: "wangwu"}}
// 每次插入一百条数据
result := db.CreateInBatches(&users, 100)
gorm.Config{}
时,如果指定了 CreateBatchSize 选项,则所有的 Insert 操作 都将遵循该选项。
更新操作
保存所有字段
先通过 First
查询出一条数据,然后对该数据进行修改并保存:
user := User{}
user.ID = 1 // 如果主键不为空则以主键为条件查询数据,否则就获取结果集中的第一条记录
db.First(&user)
user.Name = "zhangsan 2"
db.Save(&user)
更新单列
根据 WHERE 条件,更新指定字段:
- Model 用来指定更新的结构体,如果该对象主键字段有值,该值将被用于构建条件
- Where 用来指定更新条件
- Update 更新单个字段
// 条件更新
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
方法支持 struct
和 map[string]interface{}
参数。当使用 struct
更新时,默认情况下,GORM 只会更新非零值的字段。
var user User
user.ID = 1
// 根据 struct 更新记录,只会更新非零值
db.Model(&user).Updates(User{Name: "hututu", Age: 8})
// 根据 map 更新记录
db.Model(&user).Updates(map[string]interface{}{"name": "huyingjun", "age": 38})
更新选定字段
想要在更新时选定、忽略某些字段,可以使用 Select、Omit。
// 使用 Map 进行 Select
var user User
user.ID = 1
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=1;
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=1;
// 使用 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=1;
// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
批量更新
如果传入的model未指定主键,则 GORM 会执行批量更新。
var user User
// 根据 struct 更新记录,只会更新非零值
db.Model(&user).Where("age < ?", 8).Updates(User{Age: 8})
// UPDATE users SET age = 8 WHERE age < 8;
// 根据 map 更新记录
db.Model(&user).Where("id IN ?", []int{2, 3}).Updates(map[string]interface{}{"age": 18})
// UPDATE users SET age=18 WHERE id IN (2, 3);
阻止全局更新
如果在没有任何约束条件的情况下执行批量更新,GORM 默认不会执行该操作,并且返回 ErrMissingWhereClause
错误。
对此,如果需要更新全表,可以加一些条件,或使用原生 SQL,或者开启 AllowGlobalUpdate
模式:
// 没有条件 会提示 WHERE conditions required ,注意: 只是这条语句不执行,下面的代码还是继续执行
db.Model(&User{}).Update("name", "zhaoliu")
// 增加额外的条件 (注意:只有表达式才会生效,只给 1 不会被执行)
db.Model(&User{}).Where("1 = 1").Update("name", "zhaoliu")
// 原生 SQL
db.Exec("UPDATE users SET name = ?", "tianqi")
// 开启 AllowGlobalUpdate 模式
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "lisi")
获取更新结果
// 通过 `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 // 更新的错误
使用 SQL 表达式更新
GORM 允许使用表达式更新列:
// product 的 ID 是 `3`
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).Updates(map[string]interface{}{"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;
db.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;
根据子查询更新
使用子查询的结果来更新数据:
db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id"))
// UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);
db.Table("users as u").Where("name = ?", "jinzhu").Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id"))
db.Table("users as u").Where("name = ?", "jinzhu").Updates(map[string]interface{}{}{"company_name": db.Table("companies as c").Select("name").Where("c.id = u.company_id")})
不触发 Hook 和时间追踪
如果想更新数据,但又不想触发 Hook 就可以通过下面的方法:
// 更新单个列
db.Model(&user).UpdateColumn("name", "hello")
// UPDATE users SET name='hello' WHERE id = 111;
// 更新多个列
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE id = 111;
// 更新选中的列
db.Model(&user).Select("name", "age").UpdateColumns(User{Name: "hello", Age: 0})
// UPDATE users SET name='hello', age=0 WHERE id = 111;
检查字段是否有变更
GORM 提供了一个 Changed
方法,它可以用于检测某个字段是否有变更 (既数据查询出来后是否有被修改),返回一个布尔值。
Changed 方法只能与 Update、Updates 方法一起使用,并且它只是检查 Model 对象字段的值与 Update、Updates 的值是否相等,如果值有变更,且字段没有被忽略,则返回 true
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
// 如果 Role 字段有变更
if tx.Statement.Changed("Role") {
return errors.New("role not allowed to change")
}
if tx.Statement.Changed("Name", "Admin") { // 如果 Name 或 Role 字段有变更
tx.Statement.SetColumn("Age", 18)
}
// 如果任意字段有变更
if tx.Statement.Changed() {
tx.Statement.SetColumn("RefreshedAt", time.Now())
}
return nil
}
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"})
// Changed("Name") => false, 因为 `Name` 没有变更
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{
"name": "jinzhu2", "admin": false,
})
// Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"})
// Changed("Name") => false, 因为 `Name` 没有变更
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"})
// Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新
删除操作
删除单条
删除一条数据时,删除对象需要指定主键,否则会触发批量删除。
var user User
user.ID = 3
// DELETE from users where id = 3;
db.Delete(&user)
// DELETE from users where id = 3 AND name = "zhangsan";
db.Where("name = ?", "zhangsan").Delete(&user)
}
根据主键删除
GORM 允许通过主键呵呵内敛条件来删除对象,支持 Int 和 String 类型的值:
// DELETE from users where id = 3;
db.Delete(&User{}, 3)
// DELETE from users where id = 3;
db.Delete(&User{}, "3")
// DELETE FROM users WHERE id IN (1,2,3);
db.Delete(&User{}, []int{1, 2, 3})
批量删除
如果指定的值不包括主键,则会执行批量删除,删除所有匹配的记录。
// DELETE from users where age < 30;
db.Where("age < ?", 30).Delete(&User{})
// DELETE from users where age < 30;
db.Delete(&User{}, "age < ?", 30)
阻止全局删除
如果在没有任何条件的情况下执行批量删除,GORM 不会执行该操作,并返回 ErrMissingWhereClause 错误
对此,你必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate 模式,例如:
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
查询操作
查询单条
GORM 提供了 First
、Task
、Last
方法,用于在数据库中检索单个对象。在查询数据库时,添加了 LIMIT 1 条件,且在没有匹配记录时,它会返回 ErrRecordNotFound
错误。
var user User
// 获取第一条记录 (主键升序)
// SELECT * FROM users ORDER BY id LIMIT 1;
db.First(&user)
// 获取一条记录(无排序)
// SELECT * FROM users LIMIT 1;
db.Take(&user)
// 获取最后一条记录 (主键降序)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
db.Last(&user)
fmt.Println(user.ID)
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
First
和 Last
会根据主键排序,分别查询第一条和最后一条记录。只有在目标 struct 是指针或通过 db.Model()
指定 model 时,该方法才有效。
此外,如果相关 model 没有定义主键,则使用 model 第一个字段进行排序。
// 查询结果集为空,因为 没有指定 Model 无法排序
result := map[string]interface{}{}
db.Table("users").First(&result)
// 可以正常查询,使用 Model() 指定了 Model
result = map[string]interface{}{}
db.Model(&User{}).First(&result)
// 可以正常查询,因不存在主键排序问题
result = map[string]interface{}{}
db.Table("users").Take(&result)
fmt.Println(result["name"])
// 没有指定主键就会用第一个字段来排序
type Language struct {
Code string
Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1
用主键检索
如果主键是数字类型,使用内联条件来检索对象。传入字符串参数时,需要特别注意 SQL 注入的问题。
var user []User
// SELECT * FROM users WHERE id = 2;
db.First(&user, 2)
// SELECT * FROM users WHERE id = 2;
db.First(&user, "2")
// SELECT * FROM users WHERE id in (1,2);
db.Find(&user, []int{1, 2})
如果主键是字符串,查询可以这样写:
var user User
// SELECT * FROM users WHERE username = "zhangsan";
db.First(&user, "username = ?", "zhangsan")
如果 model 对象中包含主键值,则会被用来构建查询条件:
var user = User{ID: 10}
// SELECT * FROM users WHERE id = 10;
db.First(&user)
var result User
// SELECT * FROM users WHERE id = 10;
db.Model(User{ID: 10}).First(&result)
查询全部
注意 如果需要查询多条,user
需要是 User 的 Slice。
var users []User
// SELECT * FROM users;
result := db.Find(&users)
result.RowsAffected // 获取查询总条数
result.Error // 获取查询错误
条件
字符串条件
// 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';
Struct & Map 条件
var user []User
// Struct
// SELECT * FROM users WHERE name = "lisi" AND age = 38 ORDER BY id LIMIT 1;
db.Where(&User{Name: "lisi", Age: 38}).First(&user)
// Map
// SELECT * FROM users WHERE name = "lisi" AND age = 38
db.Where(map[string]interface{}{"name": "lisi", "age": 38}).Find(&user)
// Slice of primary kays
// SELECT * FROM users WHERE id IN (1, 3);
db.Where([]int{1, 3}).Find(&user)
fmt.Println(user)
0,'',false
值时,将不会被用于构造查询条件。但 Map 构建查询条件时,它将包含所有键值作为查询条件。
// SELECT * FROM users WHERE name = "jinzhu";
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
指定结构体查询字段
当使用 struct 构建查询条件时,可以额外传递字段名来指定查询条件字段:
// SELECT * FROM users WHERE name = "lisi" AND age = 0;
db.Where(&User{Name: "lisi"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;
db.Where(&User{Name: "lisi"}, "Age").Find(&users)
内联条件
将 Where 查询条件内联到 First
和 Find
方法中。
var user []User
// 如果主键是非整数类型,则通过主键获取
// SELECT * FROM users WHERE id = 'zhangsan';
db.First(&user, "id = ?", "zhangsan")
// 字符串 WHERE
// SELECT * FROM users WHERE name = "zhangsan";
db.Find(&user, "name = ?", "zhangsan")
// SELECT * FROM users WHERE name = "zhangsan" AND age > 38;
db.Find(&user, "name <> ? AND age > ?", "zhangsan", 38)
// Struct
// SELECT * FROM users WHERE age = 18;
db.Find(&user, User{Age: 18})
// Map
// SELECT * FROM users WHERE age = 18;
db.Find(&user, map[string]interface{}{"age": 18})
fmt.Println(user)
Not 条件
与 Where 一样,只是取反了。
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;
db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
db.Not([]int64{1,2,3}).First(&user)
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 = 'zhansan'").Or(User{Name: "lisi", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'zhansan' OR (name = 'lisi' AND age = 18);
// Map
db.Where("name = 'zhansan'").Or(map[string]interface{}{"name": "lisi", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'zhansan' OR (name = 'lisi' AND age = 18);
查询指定字段
SELECT 指定查询字段,默认将查询所有字段
var user []User
// SELECT name, age FROM users;
db.Select("name", "age").Find(&user)
// SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&user)
// SELECT COALESCE(age,'42') FROM users;
db.Table("user").Select("COALESCE(age,?)", 18).Rows()
fmt.Println(user)
Order BY
使用 Order BY 构建排序语句
var user []User
// SELECT * FROM users ORDER BY age desc, name;
db.Order("age desc, name").Find(&user)
// 也可以多次调用,效果同上
db.Order("age desc").Order("name").Find(&user)
// 构建自定义排序
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)
db.Clauses(clause.OrderBy{
Expression: clause.Expr{
SQL: "FIELD(id,?)",
Vars: []interface{}{[]int{1, 2, 3}},
WithoutParentheses: true},
}).Find(&user)
fmt.Println(len(user))
Limit & Offset
使用 Limit 限制返回条数或配合 Offset 实现分页
// SELECT * FROM users LIMIT 3;
db.Limit(3).Find(&users)
// 使用 Limit 限制返回条数后,还可以通过 -1 取消限制
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)
// 跳过三条返回数据
// SELECT * FROM users OFFSET 3;
db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;
db.Limit(10).Offset(5).Find(&users)
// 取消 Offset 限制
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
Group By & Having
- 使用
Group
对数据进行分组 - 使用
Having
对分组后的数据进行过滤
var userGroup []UserGroup
// SELECT age count(1) as total FROM users GROUP BY age
db.Model(&User{}).Select("age", "count(1) as total").Group("age").Find(&userGroup)
// SELECT age count(1) as total FROM users GROUP BY age Having total > 1
db.Model(&User{}).Select("age", "count(1) as total").Group("age").Having("total > ?", 1).Find(&userGroup)
fmt.Println(userGroup)
Distinct 去重
对查询的结果集进行去重
var user []User
// 对查询结果进行去重
// SELECT DISTINCT age FROM users;
db.Distinct("age").Find(&user)
fmt.Println(user)
Joins
user_infos 表结构如下:
mysql> desc user_infos;
+------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| created_at | datetime(3) | YES | | NULL | |
| updated_at | datetime(3) | YES | | NULL | |
| deleted_at | datetime(3) | YES | | NULL | |
| user_id | bigint(20) unsigned | YES | | NULL | |
| email | varchar(50) | YES | | NULL | |
| phone | varchar(15) | YES | | NULL | |
+------------+---------------------+------+-----+---------+----------------+
7 rows in set (0.03 sec)
编写代码:
var info map[string]interface{}
db.Table("users").Select("email").Joins("LEFT JOIN user_infos on users.id = user_infos.user_id").Not(map[string]interface{}{"user_infos.phone": nil}).Find(&info)
fmt.Printf("%#v", info) // map[string]interface {}{"email":"admin@qq.com"}
高级查询
智能选择字段
GORM 允许通过 Select
方法选择特定的字段,如果需要在应用程序中频繁使用 Select
,则可以定义一个小的结构体,以实现 Api 调用时 自动选择特定的字段,比如:
// 定义结构体
type User struct {
gorm.Model
Name string `gorm:"type:longtext"`
Age string `gorm:"type:int"`
Birthday time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
}
// 定义一个只查询 Name 的结构体
type ApiUser struct {
Name string
}
// SELECT `users`.`name` FROM `users` WHERE id = 1 AND `users`.`deleted_at` IS NULL
db.Model(&User{}).Where("id = ?", 1).Find(&api)
子查询
子查询可以嵌套在查询中,GORM 允许使用 *gorm.DB
对象作为参数时生成子查询
var users []User
// SELECT * FROM `users` WHERE age > (SELECT AVG(age) FROM `users`) AND `users`.`deleted_at` IS NULL
db.Where("age > (?)", db.Table("users").Select("AVG(age)")).Find(&users)
subQuery := db.Table("users").Select("AVG(age)")
db.Where("age > (?)", subQuery).Find(&users)
for _, item := range users {
fmt.Printf("%#v\n", item.Name)
}
From 子查询
GORM 允许在 Table
方法中通过 FROM 子句中使用子查询,例如:
var users []User
// SELECT * FROM (SELECT `name`,`age`,`deleted_at` FROM `users` WHERE `users`.`deleted_at` IS NULL) as u WHERE age = 18 AND `u`.`deleted_at` IS NULL
subQuery := db.Model(&User{}).Select("name", "age", "deleted_at")
db.Table("(?) as u", subQuery).Where("age = ?", 18).Find(&users)
Group 条件
使用 Group 条件可以编写更复杂的 SQL
var users []User
// 查询 18 岁以上的男性用户 或 18岁以下的女性用户
// SELECT * FROM `users` WHERE ((sex = 1 AND age > 18) OR (sex = 0 AND age < 18)) AND `users`.`deleted_at` IS NULL
db.Where(
db.Where("sex = ?", 1).Where("age > ?", 18),
).Or(
db.Where("sex = ?", 0).Where("age < ?", 18),
).Find(&users)
多列 In 查询
// SELECT * FROM `users` WHERE (name,age) IN (('lisi',18),('zhaoliu',15)) AND `users`.`deleted_at` IS NULL
db.Where("(name,age) IN ?", [][]interface{}{{"lisi", 18}, {"zhaoliu", 15}}).Find(&users)
命名参数
GORM 支持以 sql.NamedArg
和 map[string]interface{}{}
形势的命名参数:
// SELECT * FROM `users` WHERE (name1 = 'zhangsan' OR name2 = 'zhangsan') AND `users`.`deleted_at` IS NULL
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "zhangsan")).Find(&users)
// SELECT * FROM `users` WHERE (name1 = 'zhhangsan' OR name2 = 'zhhangsan') AND `users`.`deleted_at` IS NULL
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "zhhangsan"}).Find(&users)
Find To Map
GORM 允许扫描结果至 map[string]interface{}
或 []map[string]interface{}
,这是需要指定 Model
或 Table
:
// 因为不是切片 所以只获取第一条
result := map[string]interface{}{}
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL
db.Model(&User{}).Find(&result)
// 查询多条
results := []map[string]interface{}{}
// 使用 Table 可以完全不用定义 Model
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL
db.Table("users").Find(&results)
FirstOrCreate
查询一条数据,如果存在就返回这条数据,如果不存在就创建这条数据(仅支持 sturct
和 map
条件)
var user User
db.Where(User{Name: "zhengqi"}).Attrs(User{Age: 21}).FirstOrCreate(&user)
// SELECT * FROM `users` WHERE `users`.`name` = "zhengqi" AND `users`.`deleted_at` IS NULL
// 如果找到了 name = zhengqi 的记录 就返回数据,且忽略 Attrs
// 如果没找到,就插入 这条数据
// INSERT INTO "users" (name, age) VALUES ("zhengqi", 21);
无论是否找到记录,Assign
都将更新数据库:
var user User
db.Where(User{Name: "lisi"}).Assign(User{Age: 22}).FirstOrCreate(&user)
// SELECT * FROM `users` WHERE `users`.`name` = "lisi" AND `users`.`deleted_at` IS NULL
// 如果找到了 name = lisi 的记录, 会根据 Assign 更新记录
// UPDATE users SET age=22 WHERE id = 1;
// 如果没找到,就根据条件和 Assign 创建这条记录
// INSERT INTO "users" (name, age) VALUES ("lisi", 22);
FirstOrInit
与 FirstOrCreate
作用类似,区别在于不会对数据库进行任何写操作,而是直接对 sturct
进行填充。
// 根据数据库结果,创建模型数据
db.Where(User{Name: "lisi"}).Attrs(User{Age: 22}).FirstOrInit(&user)
// 根据数据库结果,填充模型数据
db.Where(User{Name: "lisi"}).Assign(User{Age: 22}).FirstOrInit(&user)
优化器、索引提示
安装 hints:go get gorm.io/hints
优化器 提示用于控制查询优化器选择某个查询执行计划,GORM 通过 gorm.io/hints
提供支持:
import "gorm.io/hints"
// 设置 SELECT 语句最大执行时间为 10000 毫秒
// SELECT * /*+ MAX_EXECUTION_TIME(10000) */ FROM `users`
db.Clauses(hints.New("MAX_EXECUTION_TIME(10000)")).Find(&User{})
索引提示 允许传递索引提示到数据库
import "gorm.io/hints"
// 建议 MySQL 使用 idx_user_name 索引
// SELECT * FROM `users` USE INDEX (`idx_user_name`)
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
// 强制 MySQL 使用 idx_user_name 和 idx_user_id 索引进行关联
// SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`,`idx_user_id`)
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
迭代
GORM 支持通过行进行迭代。(个人理解就是查询结果集 然后遍历获取)
rows, err := db.Model(&User{}).Where("age > ?", 18).Rows()
defer rows.Close()
if err != nil {
// 进行错误处理
}
// 迭代获取
for rows.Next() {
var user User
db.ScanRows(rows, &user)
fmt.Println(user.ID)
}
FindInBatches
分批查询数据并处理
var usres []User
// 每次批量处理 500 条
result := db.Where("sex = ?", 1).FindInBatches(usres, 500, func(tx *gorm.DB, batch int) error {
for _, item := range usres {
// 批量处理找到的记录
fmt.Println(item.Name)
}
// 保存
tx.Save(&usres)
// 本次批量操作影响的记录数
fmt.Println(tx.RowsAffected)
// 批量处理的次数 1、2、3...
fmt.Println(batch)
// 如果返回了 error ,会终止后续的批量操作
return nil
})
// 返回的错误
fmt.Println(result.Error)
// 整个批量操作影响的行数
fmt.Println(result.RowsAffected)
Pluck
用于在数据库中查询单个列,并将结果扫描到切片中。(类似于 PHP 的 array_column
, 从数据库中取出一列作为一维数组)
var (
uids []uint
names, names2 []string
)
// 获取所有用户 id
db.Table("users").Pluck("id", &uids)
// 获取所有用户名
db.Table("users").Pluck("name", &names)
// 获取去重后的用户名
// SELECT DISTINCT `name` FROM `users`
db.Table("users").Distinct().Pluck("name", &names2)
Scopes
GORM 允许通过 Scopes
对查询逻辑进行复用,这种共享逻辑需要定义为 func(*gorm.DB) *gorm.DB
类型。
(个人理解有点像 GORM 查询中间件,但是GORM 称这个为作用域)
例如:分页、动态表名等,更多内容参考官方文档:scopes
动态表名
// 定义 TableName 用于获取表名
func (user *User) TableName() string {
return "user"
}
// 定义表名组装作用域
func TableOfYear(user *User, year int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
tableName := user.TableName() + strconv.Itoa(year)
return db.Table(tableName)
}
}
var user *User
// 使用作用域
// SELECT * FROM `user2021` WHERE `user2021`.`deleted_at` IS NULL
db.Scopes(TableOfYear(user, 2021)).Find(&user)
Count
获取匹配的记录数
var count int64
// SELECT count(*) FROM `users` WHERE sex = 1 AND `users`.`deleted_at` IS NULL
db.Model(&User{}).Where("sex = ?", 1).Count(&count)
// SELECT count(*) FROM `users`
db.Table("users").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`
db.Table("users").Distinct("name").Count(&count)
SQL 构建器
原生 SQL
原生查询和 Scan
var result Result
db.Raw("SELECT id,name,age FROM users WHERE name = ?", "lisi").Scan(&result)
var age int
db.Raw("SELECT SUM(age) FROM users WHERE sex = ?", 1).Scan(&age)
// 更新并返回更新后的数据(MySQL不支持)
var users []User
db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)
Exec 原生SQL
// 删除用户详情表
db.Exec("DROP TABLE user_info")
// 修改 id 1,2,3 的用户年龄为 18 岁
db.Exec("UPDATE users SET age = ? WHERE id IN ?", 18, []int{1, 2, 3})
// EXEC 和 SQL 表达式
db.Exec("UPDATE users SET age = ? WHERE name = ?", gorm.Expr("age + ?", 1), "lisi")
DryRun 模式
在不执行的情况下,生成 SQL 及其参数 可以用于准备或生成测试的 SQL :
var user User
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
// SELECT * FROM `users` WHERE `users`.`id` = ? AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
fmt.Printf(stmt.SQL.String())
// [1]
fmt.Println(stmt.Vars)
ToSQL
返回生成的 SQL 但不执行,可以自动转义参数避免 SQL 注入,但不保证SQL的安全,请只用于调试.
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
// SELECT * FROM `users` WHERE id = 100 AND `users`.`deleted_at` IS NULL ORDER BY age desc LIMIT 10
fmt.Println(sql)
Row & Rows
获取 sql.Row
单条结果
var (
name string
age int
)
// 用 GORM API 构建 SQL
row := db.Table("users").Where("name = ?", "lisi").Select("name", "age").Row()
row.Scan(&name, &age)
// 使用原生SQL
row2 := db.Raw("SELECT name,age FROM users WHERE name = ?", "lisi").Row()
row2.Scan(&name, &age)
获取 sql.Rows
多条结果
var (
name string
age int
)
// 用 GORM API 构建 SQL
rows, err := db.Table("users").Where("sex = ?", 1).Select("name", "age").Rows()
defer rows.Close()
if err != nil {
// 错误处理
}
for rows.Next() {
rows.Scan(&name, &age)
// 业务逻辑处理
}
// 使用原生SQL
row2, err2 := db.Raw("SELECT name,age FROM users WHERE sex = ?", 1).Rows()
defer rows.Close()
if err2 != nil {
// 错误处理
}
for row2.Next() {
row2.Scan(&name, &age)
// 业务逻辑处理
}
Rows 扫描至 Struct
// 用 GORM API 构建 SQL
rows, err := db.Table("users").Where("sex = ?", 1).Select("name", "age").Rows()
defer rows.Close()
if err != nil {
// 错误处理
}
var user User
for rows.Next() {
db.ScanRows(rows, &user)
// 业务逻辑处理
}
连接
在同一个数据库 TCP 连接中执行多个 SQL (不在事务中)
var user User
db.Connection(func(tx *gorm.DB) error {
tx.Exec("UPDATE users SET age = 18 WHERE id = ?", 1)
tx.First(&user)
return nil
})
fmt.Println(user.Name)
高级
子句 clause
GORM 内部使用 SQL builder 生成 SQL 。对于每个操作 GORM 实际都会创建一个和 *gorm.Statement
对象,所有的 GORM API 都是在为 statement
添加 / 修改 Clause
,最后 GORM 会根据 这些 Clause 生成 SQL。
这部分应用极少,有需要可参考文档:https://gorm.io/zh_CN/docs/sql_builder.html#%E5%AD%90%E5%8F%A5%EF%BC%88Clause%EF%BC%89
关联操作
Belongs To
belongs to
会与另一个模型建立一对一连接,这种模型的每一个实例都属于另一个模型的实例。
例如,有用户(附属表)和公司(主表)两张表,但每个用户只能有一个公司。下面的类型就是表示的这种关系。
// User 属于 Company
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}
一对一关联
Has One
与另一个模型建立了一对一的关联,但和一对一关联有所区别。这种模型的每一个实例都包含或拥有另一个模型的一个实例。
(这种关联更像是 Laravel 中的一对一关联关系)
// 用户表有一张用户详情表,用户详情表的 user_id 是外键
type User struct {
gorm.Model
Name string
UserInfo UserInfo
}
type UserInfo struct {
UserID uint
Email string
Address string
}
一对多关联
Has Many
一对多关联关系,例如: 一个用户可以有多张信用卡
结构体声明
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
查询
// 检索用户列表并预加载信用卡
func GetAll(db *gorm.DB) ([]User, error) {
var users []User
err := db.Model(&User{}).Preload("CreditCards").Find(&users).Error
return users, err
}
多对多关联
Many To Many
多对多关联,如果使用 gorm.AutoMigrate()
创建表时,GORM 会自动创建中间表。
有用户表 User
和 Language
多对多结构体声明
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
}
上面的方式仅支持 从用户查询语言,如果声明反向引用则可以额外支持从语言表查询用户。
// User 拥有并属于多种 language,`user_languages` 是连接表
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
func GetAllUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Model(&User{}).Preload("Languages").Find(&users).Error
return users, err
}
// 检索 Language 列表并预加载 User
func GetAllLanguages(db *gorm.DB) ([]Language, error) {
var languages []Language
err := db.Model(&Language{}).Preload("Users").Find(&languages).Error
return languages, err
}
重写外键
GORM 提供自定义外键名称的方式,通过 foreignKey
来实现:
type User struct {
gorm.Model
Name string `gorm:"type:longtext"`
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"`
}
type Company struct {
ID int
Name string
}
重写引用
对于 belongs to
关系来说,GORM 通常使用数据库表,主表(拥有者,上例中的 Company
)的主键值作为外键参考。
如果关联字段设计在了 User
表中,那么 GORM 会自动把 Company 中的 ID 属性保存到 User 的 CompanyID 属性中。
也支持通过标签来重新定义:references
type User struct {
gorm.Model
Name string `gorm:"type:longtext"`
CompanyRefer int
Company Company `gorm:"references:Code"`
}
type Company struct {
ID int
Code string
Name string
}
references
指定:type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:CompanyID"` // use Company.CompanyID as references
}
type Company struct {
CompanyID int
Code string
Name string
}
多态关联
GORM 为 Has One 和 Has Many 提供了多态关联支持。直白的说:就是在关联表中同时存储 关联 ID
和 关联结构体标识(例如表名称),主要用途是当关联数据来自于多张表的时候,可以通过来源表+id 的方式来分别数据属于那个表的关联。
结构体定义
使用 polymorphic
指定多态字段前缀
// 员工表
type Employee struct {
ID uint
Name string
Contact Contact `gorm:"polymorphic:Owner;"`
}
// 客户表
type Customer struct {
ID uint
Name string
Contact Contact `gorm:"polymorphic:Owner;"`
}
// 联系方式表
type Contact struct {
ID uint
Email string
Phone string
WeiXin string
OwnerID uint // 与 polymorphic 值对应为 xxxID
OwnerType string // 与 polymorphic 值对应为 xxxType
}
对于表结构如下:
mysql> desc employees;
+-------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| name | longtext | YES | | NULL | |
+-------+---------------------+------+-----+---------+----------------+
2 rows in set (0.03 sec)
mysql> desc customers;
+-------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| name | longtext | YES | | NULL | |
+-------+---------------------+------+-----+---------+----------------+
2 rows in set (0.03 sec)
mysql> desc contacts;
+------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| email | longtext | YES | | NULL | |
| phone | longtext | YES | | NULL | |
| wei_xin | longtext | YES | | NULL | |
| owner_id | bigint(20) unsigned | YES | | NULL | |
| owner_type | longtext | YES | | NULL | |
+------------+---------------------+------+-----+---------+----------------+
6 rows in set (0.04 sec)
写入
// 写入员工数据
// INSERT INTO `employees` (`name`) VALUES ("张三")
// INSERT INTO `contacts` (`email`,`phone`,`wei_xin`,`owner_id`,`owner_type`) VALUES ("zhangsan@qq.com","15330000000","zhangsan","1","employees")
db.Create(&Employee{Name: "张三", Contact: Contact{
Email: "zhangsan@qq.com",
Phone: "15330000000",
WeiXin: "zhangsan",
}})
// 写入客户数据
// INSERT INTO `customers` (`name`) VALUES ("李四")
// INSERT INTO `contacts` (`email`,`phone`,`wei_xin`,`owner_id`,`owner_type`) VALUES ("lisi@qq.com","15336666666","lisi","1","customers")
db.Create(&Customer{Name: "李四", Contact: Contact{
Email: "lisi@qq.com",
Phone: "15336666666",
WeiXin: "lisi",
}})
可以通过 polymorphicValue
来指定多态类型的值:
type Employee struct {
ID uint
Name string
Contact Contact `gorm:"polymorphic:Owner;polymorphicValue:yuangong"`
}
// INSERT INTO `employees` (`name`) VALUES ("张三")
// INSERT INTO `contacts` (`email`,`phone`,`wei_xin`,`owner_id`,`owner_type`) VALUES ("zhangsan@qq.com","15330000000","zhangsan","1","yuangong")
db.Create(&Employee{Name: "张三", Contact: Contact{
Email: "zhangsan@qq.com",
Phone: "15330000000",
WeiXin: "zhangsan",
}})
Has Many 一对多
当一个员工有多条联系方式时,联系方式表就需要写入多条。
// 员工表
type Employee struct {
ID uint
Name string
// 使用 slice 表示一对多
Contact []Contact `gorm:"polymorphic:Owner;"`
}
// 写入员工数据
// INSERT INTO `employees` (`name`) VALUES ("张三")
// INSERT INTO `contacts` (`email`,`owner_id`,`owner_type`) VALUES ("zhangsan@qq.com","1","employees"),("zhangsan@foxmail.com","2","employees")
db.Create(&Employee{Name: "张三", Contact: []Contact{{Email: "zhangsan@qq.com"}, {Email: "zhangsan@foxmail.com"}}})
外键约束
可以通过标签 constraint
配置 OnUpdate
、OnDelete
实现外键约束,在使用 GORM 进行迁移时它会被创建,。
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
实体关联
概念定义:例如有用户表、用户语言关联表、语言表 三张表,然后我们通过用户对关联的语言进行操作。对于这样的关联关系,我们称通过用户表查询的结构体变量为 "源模型",将语言表称之为关联,将用户语言关联表(中间表)称之为“关系”
自动创建、更新
在创建或更新记录时,GORM 会通过 Upsert(更新或插入) 自动保存并关联其他引用数据。
user := User{
Name: "jinzhu",
BillingAddress: Address{Address1: "Billing Address - Address 1"},
ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
Emails: []Email{
{Email: "jinzhu@example.com"},
{Email: "jinzhu-2@example.com"},
},
Languages: []Language{
{Name: "ZH"},
{Name: "EN"},
},
}
db.Create(&user)
// BEGIN TRANSACTION;
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "languages" ("name") VALUES ('ZH'), ('EN') ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "user_languages" ("user_id","language_id") VALUES (111, 1), (111, 2) ON DUPLICATE KEY DO NOTHING;
// COMMIT;
db.Save(&user)
如果想要插入主数据,同时 Upsert 关联数据,可以使用 FullSaveAssociations
模式:
db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)
// ...
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY SET address1=VALUES(address1);
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY SET email=VALUES(email);
// ...
跳过自动创建、更新
如果想要在创建、更新时跳过自动保存,可以使用 Select
(选择某些字段) 或 Omit
(跳过某些字段) :
user := User{
Name: "jinzhu",
BillingAddress: Address{Address1: "Billing Address - Address 1"},
ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
Emails: []Email{
{Email: "jinzhu@example.com"},
{Email: "jinzhu-2@example.com"},
},
Languages: []Language{
{Name: "ZH"},
{Name: "EN"},
},
}
// 只插入 Users 表的 name 字段
// INSERT INTO "users" (name) VALUES ("jinzhu", 1, 2);
db.Select("Name").Create(&user)
// 跳过 BillingAddress 创建
db.Omit("BillingAddress").Create(&user)
// 创建用户时跳过所有关联
db.Omit(clause.Associations).Create(&user)
关联模式
通过关联模式(Association
),可以轻松处理一些关联操作
// user 是原模型(我理解为主表)
// 关系字段是 Languages
var user User
// 如果满足上面两个需求,就会开始关联模式
db.Model(&user).Association("Languages")
// 如果不满足 可以通过 Error 获取错误
adb := db.Model(&user).Association("Languages_Test")
if adb.Error != nil {
// unsupported relations: Languages_Test
fmt.Println(adb.Error)
}
查找关联
先查询出主表数据,然后根据关联字段 users.id
关联查询数据查询用户所拥有的语言:
// 通过主键获取数据填充 user
// SELECT * FROM `users` WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL
db.Find(&user, 1)
// 通过 user 关联数据
// 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
db.Model(user).Association("Languages").Find(&language)
// 输出用户的语言
for _, v := range language {
fmt.Println(v.Name)
}
带条件的关联
// 带查询条件
db.Model(user).Where("Languages.id = ?", 1).Association("Languages").Find(&language)
// 带字段排序
db.Model(user).Order("Languages.id DESC").Association("Languages").Find(&language)
添加关联
多对多或一对多关联 则添加新的关联,如果是一对一关联,则是替换掉当前关联
var user User
var language Language
db.Find(&user, 1)
db.Last(&language) // 查询出最后一条
// 添加关联
db.Model(&user).Association("Languages").Append([]Language{language})
// 直接添加一个新语言并设置关联
db.Model(&user).Association("Languages").Append(&Language{Name: "JP"})
替换关联
使用一个新的关联替换现有关联,与添加关联的区别是会先删除掉现有关联,然后重新添加关联关系。
var user User
var language Language
db.Find(&user, 1)
db.Last(&language) // 查询出最后一条
// 替换关联
db.Model(&user).Association("Languages").Replace([]Language{language})
删除关联
如果存在关联就删除关联关系,但不会删除关联的数据。
var user User
var language Language
db.Find(&user, 1)
db.Last(&language) // 查询出最后一条
// 删除关联
db.Model(&user).Association("Languages").Delete([]Language{language})
清空关联
清空原模型与关联直接的所有引用关系,但不会删除关联。
var user User
db.Find(&user, 1)
// 清空所有关联
db.Model(&user).Association("Languages").Clear()
关联统计
统计关联条数
var user User
var language Language
db.Find(&user, 1)
db.Last(&language) // 查询出最后一条
// 统计关联
count := db.Model(&user).Association("Languages").Count()
// 关联条数: 1
fmt.Printf("关联条数: %d", count)
批量处理
通过关联模式也可以批量操作数据
// 查询所有用户的所有角色
db.Model(&users).Association("Role").Find(&roles)
// 从所有 team 中删除 user A
db.Model(&users).Association("Team").Delete(&userA)
// 获取去重的用户所属 team 数量
db.Model(&users).Association("Team").Count()
// 对于批量数据的 `Append`、`Replace`,参数的长度必须与数据的长度相同,否则会返回 error
var users = []User{user1, user2, user3}
// 例如:现在有三个 user,Append userA 到 user1 的 team,Append userB 到 user2 的 team,Append userA、userB 和 userC 到 user3 的 team
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// 重置 user1 team 为 userA,重置 user2 的 team 为 userB,重置 user3 的 team 为 userA、 userB 和 userC
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
带 Select 的删除
在删除记录时,可以通过 Select
删除具有 一对一、一对多、多对多 关联关系的记录:
A
var user User
db.Find(&user, 1)
// 删除用户时,同时删除用户语言的关联关系
db.Select("Languages").Delete(&user)
// 删除用户,删除用户的所有关联关系(包含一对一、一对多、多对多)
db.Select(clause.Associations).Delete(&user)
// 删除多个用户的语言关联关系
var users []User
db.Find(&users)
db.Select("Languages").Delete(&users)
关联标签
标签 | 描述 |
---|---|
foreignKey | 指定当前模型的列作为连接表的外键 |
references | 指定引用表的列名,其将被映射为连接表外键 |
polymorphic | 指定多态类型,比如模型名 |
polymorphicValue | 指定多态值、默认表名 |
many2many | 指定连接表表名 |
joinForeignKey | 指定连接表的外键列名,其将被映射到当前表 |
joinReferences | 指定连接表的外键列名,其将被映射到引用表 |
constraint | 关系约束,例如:OnUpdate 、OnDelete |
预加载
Preload 预加载
GORM 允许通过 Preload
在查询原模型的时候,预加载关联数据。
var user User
// 预加载
// SELECT * FROM `user_languages` WHERE `user_languages`.`user_id` = 1
// SELECT * FROM `languages` WHERE `languages`.`id` = 2 AND `languages`.`deleted_at` IS NULL
// SELECT * FROM `users` WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL
db.Debug().Preload("Languages").Find(&user, 1)
for _, v := range user.Languages {
fmt.Println(v.Name)
}
Joins 预加载
Preload
是使用一个单独的查询加载关联数据。而 Join Preload
则会通过 left join
加载关联数据。
var user User
// Joins 预加载,注意 这玩意仅支持一对一
db.Joins("UserInfo").Find(&user, 1)
预加载全部
与使用 Select
类似,支持使用 clause.Associations
来预加载全部关联。
// 预加载全部
db.Preload(clause.Associations).Find(&user, 1)
注意:clause.Associations
不会预加载嵌套的关联,但可以通过多次调用实现嵌套预加载。
db.Preload("Orders.OrderItems.Product").Preload(clause.Associations).Find(&users)
带条件的预加载
允许带过滤条件的 Preload
关联:
var user User
db.Find(&user, 1)
// 在预加载时添加额外的过滤条件
// SELECT * FROM `user_languages` WHERE `user_languages`.`user_id` = 1
// SELECT * FROM `languages` WHERE `languages`.`id` IN (1,2) AND status = 1 AND `languages`.`deleted_at` IS NULL
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 1
db.Debug().Preload("Languages", "status = ?", 1).Find(&user)
自定义预加载 SQL
通过 func(db *gorm.DB) *gorm.DB
闭包实现自定义预加载 SQL,例如:
// 通过闭包在预加载的时候自定义关联查询的 SQL
// SELECT * FROM `languages` WHERE `languages`.`id` IN (1,2) AND `languages`.`deleted_at` IS NULL ORDER BY id DESC
db.Debug().Preload("Languages", func(db *gorm.DB) *gorm.DB {
return db.Order("id DESC")
}).Find(&user)
嵌套预加载
GORM 支持嵌套预加载,例如:
db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users)
// 自定义预加载 `Orders` 的条件
// 这样,GORM 就不会加载不匹配的 order 记录
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)
其他
Context 上下文
GORM 通过 WithContext
方法提供了 Context 的支持, 通过上下文我们可以控制 GORM 的执行 SQL 超时设置。
单会话模式
单会话模式被用来执行单次操作。
db.WithContext(ctx).Find(&users)
持续会话模式
持续会话模式通常被用来执行一系列操作。
tx := db.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("name", "dbkuaizi")
Context 超时
对于执行时间较长的 SQL,可以传入一个带超时的 context
给 db.WithContext
来设置是超时时间:
// 设置执行超过 2 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var users []User
db.WithContext(ctx).Find(&users)
Hooks/Callbacks
也可以从当前 Statement
中获取 Context 对象:
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
ctx := tx.Statement.Context
// ...
return
}
错误处理
因为错误处理在 Go 中非常重要,所以 GORM 鼓励在所有的终结方法后都进行错误处理。
Error
GORM 的错误处理与其他常见 Go 代码有所不同,这是因为 GORM 提供的是链式 API 调用。
如果遇到任何错误,GORM 都会设置 *gorm.DB
的 Error
字段,在查询后直接判断就可以了:
var user User
// 直接获取 Error
if err := db.Where("name = ?", "dbkuaizi").First(&user).Error; err != nil {
// 处理错误
}
// 从 Result 获取
if result := db.Where("name = ?", "dbkuaizi").First(&user); result.Error != nil {
// 处理错误
}
ErrRecordNotFound
使用 First
、Last
、Take
方法找不到记录时,GORM 会返回 ErrRecordNotFound
错误。如果发生了多个错误,可以通过 errors.Is
判断错误是否为 ErrRecordNotFound
例如:
// 检查错误是否为 RecordNotFound
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)
链式调用
GORM 允许链式操作:
db.Where("name = ?", "jinzhu").Where("age = ?", 18).First(&user)
GORM 包含三种类型的方法:链式方法
、终结方法
、新建会话方法
。
在 链式方法
、终结方法
之后,GORM 会返回一个初始化的 *gorm.DB
实例,需要注意的是实例不能被安全的重复使用,因为新生成的 SQL 可能会被之前的条件污染,例如:
queryDB := DB.Where("name = ?", "jinzhu")
queryDB.Where("age > ?", 10).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10
queryDB.Where("age > ?", 20).First(&user2)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20
新建会话方法
来共享 *gorm.DB
为了重新使用初始化的 *gorm.DB
,可以使用 新建会话方法
创建一个可共享的 *gorm.DB
:
queryDB := DB.Where("name = ?", "jinzhu").Session(&gorm.Session{})
queryDB.Where("age > ?", 10).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10
queryDB.Where("age > ?", 20).First(&user2)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 20
Session
GORM 提供了 Session
方法,这是一个 New Session Method
,它允许创建自带配置的新建会话模式:
Session 可配置项如下:
``go
// Session 配置
type Session struct {
DryRun bool
PrepareStmt bool
NewDB bool
Initialized bool
SkipHooks bool
SkipDefaultTransaction bool
DisableNestedTransaction bool
AllowGlobalUpdate bool
FullSaveAssociations bool
QueryFields bool
Context context.Context
Logger logger.Interface
NowFunc func() time.Time
CreateBatchSize int
}
``
DryRun
生成 SQL 但不执行,可用于准备或测试生成的 SQL :
var user User
// 新建会话模式
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
fmt.Println(stmt.SQL.String()) // SELECT * FROM `users` WHERE `users`.`id` = ?
fmt.Printf("%#v\n", stmt.Vars) // []interface {}{1}
// 全局 DryRun 模式
sqliteDB, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{DryRun: true})
// 不同的数据库生成不同的 SQL
stmt := db.Find(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 // PostgreSQL
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = ? // MySQL
stmt.Vars //=> []interface{}{1}
可以通过 *gorm.DB.Dialector.Explain()
来生成的最终的 SQL(注意是生成,不是执行) :
var user User
// 新建会话模式
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
// 生成最终 SQL
execSQL := db.Dialector.Explain(stmt.SQL.String(), stmt.Vars...)
// SELECT * FROM `users` WHERE `users`.`id` = 1
fmt.Println(execSQL)
预编译
PrepareStmt
在执行任何 SQL 时都会创建一个 prepared statement
并将其缓存,以提高后续的效率:
// 全局模式,所有 DB 操作都会创建并缓存预编译语句
db, _ := gorm.Open(mysql.New(mysql.Config{
DSN: "root:root@(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local",
}), &gorm.Config{
PrepareStmt: true,
})
var user User
// 会话模式
tx := db.Session(&gorm.Session{PrepareStmt: true})
tx.First(&user)
tx.Model(&user).Update("Name", "dbkuaizi")
// 返回预编译语句管理器
stmtManger, ok := tx.ConnPool.(*gorm.PreparedStmtDB)
if ok {
// 关闭当前会话的预编译模式
stmtManger.Close()
}
// 打印出当前会话的预编译 SQL
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
// UPDATE `users` SET `name`=?,`updated_at`=? WHERE `users`.`deleted_at` IS NULL AND `id` = ?
fmt.Println(stmtManger.PreparedSQL)
// 当前数据库连接池的(所有会话)开启预编译模式
for sql, stmt := range stmtManger.Stmts {
fmt.Println(sql) // 预编译 SQL
fmt.Println(stmt) // 预编译模式
stmt.Close() // 关闭预编译模式
}
NewDB
通过 NewDB
选项创建一个不带之前 Where
条件的新 DB, 例如:
tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{NewDB: true})
tx.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1
tx.First(&user, "id = ?", 10)
// SELECT * FROM users WHERE id = 10 ORDER BY id
// 不带 `NewDB` 选项
tx2 := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
tx2.First(&user)
// SELECT * FROM users WHERE name = "jinzhu" ORDER BY id
跳过钩子
如果想在执行当前数据库操作时跳过钩子,可以使用 SkipHooks
模式:
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)
DB.Session(&gorm.Session{SkipHooks: true}).Find(&user)
DB.Session(&gorm.Session{SkipHooks: true}).Delete(&user)
DB.Session(&gorm.Session{SkipHooks: true}).Model(User{}).Where("age > ?", 18).Updates(&user)
AllowGlobalUpdate
在默认的情况下,GORM 是不允许全局 update/delete,它会返回 ErrMissingWhereClause
错误,这是可以将该选项设置为 true 以允许全局操作:
// WHERE conditions required
db.Model(&User{}).Update("name", "dbkuaizi")
// 正常执行无错误提示
db.Session(&gorm.Session{AllowGlobalUpdate: true}).
Model(&User{}).Update("name", "dbkuaizi")
FullSaveAssociations
当创建或更新记录时,GORM 将使用 Upsert 自动保存关联和其引用。如果想要更新关联的数据,应该使用 FullSaveAssociations
模式:
db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)
// ...
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY SET address1=VALUES(address1);
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY SET email=VALUES(email);
// ...
Context
通过 Context
选项,可以传入 Context
来追踪 SQL 操作:
// 设置超时限制
timeoutCtx, _ := context.WithTimeout(context.Background(), time.Second)
tx := db.Session(&Session{Context: timeoutCtx})
tx.First(&user) // 带 timeoutCtx 的查询
tx.Model(&user).Update("role", "admin") // 带 timeoutCtx 的更新
也提供了快捷调用方法 WithContext
,其使用方法如下:
func (db *DB) WithContext(ctx context.Context) *DB {
return db.Session(&Session{Context: ctx})
}
NowFunc
允许通过 NowFunc
改变 GORM 获取 当前时间的实现:
db.Session(&gorm.Session{
NowFunc: func() time.Time {
// 获取时间戳并转换为本地时间
return time.Now().Local()
},
})
调试 Debug
Debug
方法只是将会话 Logger
修改为调试模式的简写形式:
func (db *DB) Debug() (tx *DB) {
tx = db.getInstance()
return tx.Session(&Session{
Logger: db.Logger.LogMode(logger.Info),
})
}
查询字段
声明查询字段,可以避免 SELECT * FROM ...
:
db.Session(&gorm.Session{QueryFields: true}).Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users` // 有该选项
// SELECT * FROM `users` // 没有该选项
批量插入条数
当设置当前会话的 CreateBatchSize
参数时,则在调用 Create 插入数据时会自动调用 CreateInBatches
批量插入操作以提高性能:
users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}
db.Session(&gorm.Session{CreateBatchSize: 1000}).Create(&users)
// INSERT INTO users xxx (需 5 次)
// INSERT INTO pets xxx (需 15 次)
钩子 Hooks
Hook 生命周期
GORM Hook 是在模型增删改查的操作前后会调用的函数,类似于 PHP 框架 ORM 的修改器/获取器。
如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。
钩子方法的函数签名应是:func(*gorm.DB) error
签名函数:定义了函数或方法的输入与输出
挂载点
插入记录
插入数据时使用的 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
// 提交或回滚事务
删除记录
删除数据时使用的 Hook 事件:
// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务
查询记录
// 从 db 中加载数据
// 预加载数据
AfterFind
修改当前操作
下面这段代码的意思是,写入用户之前判断用户的这个角色是否存在,如果不存在就返回 err 不允许写入。
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
}
事务
禁用默认事务
为了保证数据的一致性,GORM 默认会把写入操作放在事务里执行(创建、更新、删除)。如果对事务没有很高的要求,可以在初始化的时候禁用掉事务,可以获得大约 30% 的性能提升(个人认为如果不涉及关联操作可以先关掉事务,然后在需要关联操作的地方单独开启)。
// 全局禁用默认事务
db, _ := gorm.Open(mysql.New(mysql.Config{
DSN: "root:root@(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local",
}), &gorm.Config{SkipDefaultTransaction: true})
var user = User{}
// 在当前会话中禁用事务
tx := db.Session(&gorm.Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Model(&user).Update("Age", 18)
自动事务
如果要在事务中执行一些操作,可以使用 db.Transaction(func(tx *gorm.DB) error {}
在返回任何错误时都会自动回滚。
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行 db 操作需要使用 tx 而不是 db
// 返回任何错误都会回滚事务,返回 nil 则会提交事务
if err := tx.Create(&User{Name: "zhangasn"}).Error; err != nil {
return err
}
if err := tx.Create(&User{Name: "lisi"}).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(tx2 *gorm.DB) error {
tx2.Create(&user3)
return nil
})
return nil
})
手动事务
如果感觉上面的方式不够灵活 也可以手动进行事务控制:
// 开始事务
tx := db.Begin()
// 在事务中执行一些操作
if err := tx.Create(&User{Name: "zhangsan"}).Error; err!= nil {
tx.Rollback() // 回滚事务
}
// 提交事务
tx.Commit()
保存点
GORM 提供了 SavePoint
和 RollbackTo
,来提供保存点以及回滚至保存点功能,例如:
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
数据迁移
GORM 提供了数据库迁移功能,可以用来自动迁移数据库的结构,以保持数据库的结构是最新的。
AutoMigrate
通过 AutoMigrate
进行迁移,会自动创建表、缺失的外键,列和索引。如果是大小、精度、是否为空等发生变更,则 AutoMigrate
会改变列的类型。出于数据保护的目的,AutoMigrate
不会删除未使用的列(既不在结构体中的表字段,不会被删除)。
// 迁移单个表
db.AutoMigrate(&User{})
// 迁移多个表
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 创建表时添加后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
在迁移数据库时,会自动创建数据库外键约束,可以在初始化时禁用此功能:
db, _ := gorm.Open(mysql.New(mysql.Config{
DSN: "root:root@(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local",
}), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
Migrator 接口
GORM 提供了 Migrator 接口,这个接口为每个数据库提供了统一的 API 接口,可以为数据库构建独立的迁移,例如:SQLite 不支持修改字段和删除字段,当你试图修改表结构时,GORM 会创建一个新表、复制所有数据、然后删除旧表、重命名新表。
如果要实现一个自定义的数据库驱动,那么只要实现了 Migrator 接口中的方法,就可以让自定义驱动支持数据库迁移。
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 // 创建索引 (索引的类型需要通过结构体 tag 设置)
DropIndex(dst interface{}, name string) error // 删除索引
HasIndex(dst interface{}, name string) bool // 检查索引是否存在
RenameIndex(dst interface{}, oldName, newName string) error // 修改索引名称
}
日志
Logger
GORM 有一个默认 Logger 实现,在默认的情况下,它会打印慢 SQL 和错误。
Logger 能接受的选项不多,可以在初始化时自定义它,例如:
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值,默认 200 ms
LogLevel: logger.Silent, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略 ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
},
)
// 全局使用
db, _ := gorm.Open(mysql.New(mysql.Config{
DSN: "root:root@(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local",
}), &gorm.Config{Logger: newLogger})
// 新建会话模式
tx := db.Session(&gorm.Session{Logger: newLogger})
var user User
tx.Find(&user)
日志级别
GORM 定义了四个日志级别:Silent
、Error
、Warn
、Info
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
Debug
通过 Debug()
可以将当前操作的 log 级别调整为 logger.Info
。
db.Debug().Where("name = ?", "dbkuaizi").First(&User{})
自定义 Logger
如果需要自定义 Logger 可以参考 GORM 默认的 logger 实现。
Logger 需要实现以下接口,它支持 Context
,可以用来追踪日志:
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 提供了一个 DB()
方法,可以从当前 *gorm.DB
中返回一个通用的数据库接口 *sql.DB
// 获取通用数据库对象 sql.DB,然后使用其提供的功能
sqlDB, err := db.DB()
// Ping 判断数据库连接状态
sqlDB.Ping()
// Close 关闭数据库连接
sqlDB.Close()
// 返回数据库统计信息
sqlDB.Stats()
*sql.DB
,它会返回错误
连接池
// 获取通用数据库对象 sql.DB ,然后使用其提供的功能
sqlDB, err := db.DB()
// SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。
sqlDB.SetMaxIdleConns(10)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)
// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)
性能
GORM 已经做了很多优化用来提升性能,对于绝大多数应用场景性能已经足够用了。但还可以通过一些操作进一步改进性能。
带缓存的SQL生成器
Prepared Statement 也可以和原生 SQL 一起用:
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
PrepareStmt: true,
})
db.Raw("select sum(age) from users where role = ?", "admin").Scan(&age)
选择字段
默认情况下,GORM 在查询时会选择所有的字段,您可以使用 Select 来指定您想要的字段。
db.Select("Name", "Age").Find(&Users{})
或者定义一个较小的结构体,使用 智能选择字段
读写分离
通过配置读写分离,提高数据量吞吐速度: GORM 读写分离官方文档
设置
GORM 提供了 Set
、Get
、InstanceSet
、InstanceGet
方法来允许用户传值给钩子或其他方法。
GORM 中的一些特性也用到了这些机制,比如迁移表结构时传递表格属性:
// 创建表时添加表后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
Set/Get
使用 Set
和 Get
传递额外的设置给钩子方法:
type User struct {
gorm.Model
CreditCard CreditCard
// ...
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
myValue, ok := tx.Get("my_value")
// ok => true
// myValue => 123
}
type CreditCard struct {
gorm.Model
// ...
}
func (card *CreditCard) BeforeCreate(tx *gorm.DB) error {
myValue, ok := tx.Get("my_value")
// ok => true
// myValue => 123
}
myValue := 123
db.Set("my_value", myValue).Create(&User{})
InstanceSet / InstanceGet
使用 InstanceSet / InstanceGet 传递设置到 *Statement
的钩子方法,例如:
type User struct {
gorm.Model
CreditCard CreditCard
// ...
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
myValue, ok := tx.InstanceGet("my_value")
// ok => true
// myValue => 123
}
type CreditCard struct {
gorm.Model
// ...
}
// 在创建关联时,GORM 创建了一个新 `*Statement`,所以它不能读取到其它实例的设置
func (card *CreditCard) BeforeCreate(tx *gorm.DB) error {
myValue, ok := tx.InstanceGet("my_value")
// ok => false
// myValue => nil
}
myValue := 123
db.InstanceSet("my_value", myValue).Create(&User{})
db.InstanceSet()
和 db.Set()
的区别在于,前者是针对于当前 SQL 构建器的,而后者是针对整个 DB 实例的。
笔记参考:官方 GORM 指南,以及个人实践后的一些理解。
注:官方文档高级主题部分实际使用场景很少,本篇笔记作为入门学习笔记,没有过多深入这部分。
实测 Gorm 的 `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` 没有生效,查看见表语句并没有 ON UPDATE CASCADE 和 ON DELETE CASCADE。
试了一下,没问题,外键约束会创建在 credit_cards 表中,查看 DDL 可以看到:CONSTRAINT `fk_users_credit_card` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE