MENU

文章目录

GORM 学习笔记

2022 年 12 月 26 日 • 阅读: 157 • 编码,Go

笔记提供 PDF 版

GORM 官方中文文档:https://gorm.io/zh_CN/docs/index.html

ORM 相比原生 SQL 提高了开发效率,但相应的牺牲了 SQL 的灵活性和性能,所以在高并发的项目上需要考虑是否适用 ORM 。

基础使用

安装 GORM

go get -u gorm.io/gorm
go get -u gorm.io/driver/xxx

xxx 表示需要适用的数据库驱动,支持 mysqlpostgressqlitesqlserverclickhouse

连接数据库

最简单的数据库连接方式

// 账户:密码@(链接地址:端口)/库名? ... 更多参数    
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
}

mysql.Config 是 GORM 链接数据库时的配置选项
gorm.Config 是针对 GROM 本身的设置选项

GORM 模型

使用 ORM 工具时,通常要在代码中定义模型 Models 与数据库中的数据表进行映射,在 GORM 中模型通常是定义的结构体、基本的 Go 类型、实现了 sql.Scannerdriver.Valuer 接口的自定义类型以及其指针或别名组成。

模型定义

为了方便定义模型, GORM 内置了一个 gorm.Model 结构体。这个结构体内置了四个字段:IDCreatedAtUpdatedAtDeletedAt

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

因为结构体是可以拿来做数据库迁移的,所以除了字段名称外可以定义很多在定义表结构时用到的属性,例如 是否是主键、是否唯一、是否为 Null、甚至可以用来创建索引

结构体标记 Struct Tags

标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializerspecifies 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关系约束,例如:OnUpdateOnDelete

名称约定

主键 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"
}

注意: TableName 不支持动态变化,它会被缓存下来以便后续使用。想要使用动态表名,你可以使用 Scopes,例如:

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 方法支持 structmap[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})

当通过 struct 更新时,GORM 只会更新非零字段。 如果您想确保指定字段被更新,你应该使用 Select 更新选定字段,或使用 map 来完成更新操作。

更新选定字段

想要在更新时选定、忽略某些字段,可以使用 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 提供了 FirstTaskLast 方法,用于在数据库中检索单个对象。在查询数据库时,添加了 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)

如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。

FirstLast 会根据主键排序,分别查询第一条和最后一条记录。只有在目标 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)

当使用 struct 构建查询条件时,GORM 只会使用非零值字段进行查询,这意味着当你的字段值为 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 查询条件内联到 FirstFind 方法中。

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.NamedArgmap[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{},这是需要指定 ModelTable

// 因为不是切片 所以只获取第一条
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

查询一条数据,如果存在就返回这条数据,如果不存在就创建这条数据(仅支持 sturctmap 条件)

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 会自动创建中间表。

有用户表 UserLanguage

多对多结构体声明

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

GORM 会通过结构体来猜测关联关系,如果两张表中有相同外键的字段,则需要通过 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 配置 OnUpdateOnDelete 实现外键约束,在使用 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关系约束,例如:OnUpdateOnDelete

预加载

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,可以传入一个带超时的 contextdb.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.DBError 字段,在查询后直接判断就可以了:

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

使用 FirstLastTake 方法找不到记录时,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)

注意:SQL 并不总是能安全地执行,GORM 仅将其用于日志,它可能导致会 SQL 注入。

预编译

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

签名函数:定义了函数或方法的输入与输出

注意 在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

挂载点

插入记录

插入数据时使用的 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 提供了 SavePointRollbackTo,来提供保存点以及回滚至保存点功能,例如:

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 定义了四个日志级别:SilentErrorWarnInfo

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 提供了 SetGetInstanceSetInstanceGet 方法来允许用户传值给钩子或其他方法。

GORM 中的一些特性也用到了这些机制,比如迁移表结构时传递表格属性:

// 创建表时添加表后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

Set/Get

使用 SetGet 传递额外的设置给钩子方法:

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 指南,以及个人实践后的一些理解。
注:官方文档高级主题部分实际使用场景很少,本篇笔记作为入门学习笔记,没有过多深入这部分。

CC版权: 本篇博文采用《CC 协议》,转载必须注明作者和本文链接