GORM 入门
五月其一,学习 go 的 ORM 框架 GORM。
GORM 是国人写的,有中文文档《GORM 指南》,写得相当不错,通读大概需要几个小时,读完直接就是 GORM 大师(不是
GORM 历史
简单写一下 GORM 的历史。
GORM 最初来自于一个快速上线的项目,里面需要拼装大量 SQL,作者拼 SQL 拼烦了,自己周末写了 GORM 第一版。
在 2013 年,GORM V1 发布在 github 上。
在 2020 年初疫情期间,作者重写了一遍 GORM,再经过几个月迭代开发出来 GORM V2。按照作者的说法:
GORM V2 是一次相对于 V1 版本的重写,在功能、性能上都得到了增强,并且将部分容易掉坑的 API 进行了调整,来减少项目出错的可能性。
在 2021 年,GORM 推出了 GEN 版本(github 地址),通过自动生成代码的方式使用 GORM,更加安全(避免 SQL 注入),但是使用的人较少,本文并不涉及该版本。
GORM 作者现在在字节跳动的架构-语言部门,字节内的主流 MySQL ORM 框架是 BytedGORM(内部封装版 GORM V2)。
本文内容基于 GORM V2 版本。
你可以在 go 代码中通过 GORM 操作数据库,比如:
1 | // 创建 |
为了让你快速上手使用 GORM,下面整理了最小示例代码。
最小示例代码
前置准备
首先你需要安装并启动数据库(MySQL)。
如果你在本地还没有配置数据库,我建议你使用 Docker,只需要一句命令就能安装并启动:
1 | docker run --name mariadb-test -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mariadb |
这里使用的是 MariaDB(可简单理解为开源版 MySQL),使用 docker 安装参考文档:《Installing and Using MariaDB via Docker》。
代码
1 | package main |
(省略 go get 获取代码依赖的部分)
执行结果
在你执行完上面的代码之后,会发生如下事情:
- 数据库将自动建表(users),你可以使用
SHOW CREATE TABLE users;
查看建表语句。 - 代码将在 users 表中创建一条数据,并查询该条数据。
- GORM 执行的每条 SQL 语句,都会打印在控制台上。
GORM 增删改查
CRUD 是最基础的功能,具体使用到的 GORM 方法整理如下:
CRUD | 最常用的方法 | 其他方法 |
---|---|---|
查询 | Find |
First /Last /Take /FindInBatches /FirstOrInit /FirstOrCreate /Count /Scan |
创建 | Create |
CreateInBatches /Save |
更新 | Updates |
Update /UpdateColumn /UpdateColumns |
删除 | Delete |
– |
有关增删该查的操作,GORM 文档写得非常清晰(《GORM CRUD 接口》),不重复抄轮子。
GORM 方法
GORM 使用链式操作,比如:
1 | db.Where("name = ?", "pz").Where("age = ?", 18).First(&user) |
GORM 的方法分为三种:
链式方法
(Chain Method)Finisher 方法
(Finisher Method)新建会话方法
(New Session Method)
链式方法
是中间操作,往 db 实例中追加条件,Finisher 方法
是终止操作,生成 SQL 并执行。
链式方法
会往同一个 db 实例中追加条件,如果同一个 db 实例使用了两个 Finisher 方法
,第二个语句会受到影响。
1 | tx := db.Where("name = ?", "pz") |
如果遇到 SQL 条件污染,返回不明错误,优先检查是否是链式方法
造成的。
新建会话方法
用来避免链式方法
造成的不安全问题,有三种方法:Session
,WithContext
,Debug
。(详见《GORM 链式方法》)
1 | // 通过 Session 方法,使 tx 变成了共享安全模式 |
链式方法
和 Finisher 方法
平常使用比较多,整理如下:
链式方法
GORM 方法 | 作用 | 代码示例 |
---|---|---|
Model | 指定查询 Model(go 定义的数据模型,对应 Table) | db.Model(&User{}).Update(“name”, “hello”) |
Clauses | 增加 Clause(一般用不上,特例特用) | db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) |
Table | 指定查询数据库 Table | db.Table(“users”).Update(“name”, “hello”) |
Distinct | SQL distinct 操作:去重 | db.Model(&User{}).Distinct(“name”).Find(&users) |
Select | SQL select 操作:选择字段 | db.Model(&User{}).Select(“name”).Find(&users) |
Omit | 忽略字段(反向 Select 操作) | db.Model(&User{}).Omit(“name”).Find(&user) |
Where | SQL where 操作:查询条件 | db.Model(&User{}).Where(“name = ?”, “pz”).Find(&user) |
Not | SQL not 操作:非 | db.Not(User{Name: “pz”, Age: 18}).First(&users) |
Or | SQL or 操作:或 | db.Where(“name = ?”, “pz”).Or(“name = ?”, “gy”).Find(&users) |
Joins | SQL left join 操作:左连接(用法很多,详见官方文档《GORM - 查询》) | db.Model(&User{}).Select(“users.name, emails.email”).Joins(“left join emails on emails.user_id = users.id”).Scan(&result{}) |
Group | SQL group by 操作:分组 | db.Model(&User{}).Select(“name, sum(age) as total”).Group(“name”).Find(&users) |
Having | SQL having 操作:筛选分组 | db.Model(&User{}).Select(“name, sum(age) as total”).Group(“name”).Having(“name = ?”, “pz”).Find(&users) |
Order | SQL order 操作:排序 | db.Order(“age desc, name”).Find(&users) |
Limit | SQL limit 操作:限制数量 | db.Limit(3).Find(&users) |
Offset | SQL offset 操作:跳过数量 | db.Offset(3).Find(&users) |
Scopes | 用于复用相同的 db 语句 | 详见《GORM - 高级查询》 |
Preload | 预加载相关表 | 详见《GORM - 预加载》 |
Attrs | 用于 FirstOrInit/FirstOrCreate 方法,当查不到数据时,采用 Attrs 赋值字段 | db.Attrs(&User{Age: 18}).FirstOrInit(&user, “name = ?”, “pz”) |
Assign | 用于 FirstOrInit/FirstOrCreate 方法,无论是否查到结果,都采用 Assign 赋值字段 | db.Assign(&User{Age: 1}).FirstOrInit(&user, “name = ?”, “pz”) |
Unscoped | 找到软删除的记录 | db.Unscoped().Where(“age = 20”).Find(&users) |
Raw | 原生 SQL | db.Raw(“SELECT name, age FROM users WHERE name = ?”, “Antonio”).Scan(&result) |
Finisher 方法
GORM 方法 | 作用 | 代码示例 |
---|---|---|
Create | 创建 | db.Create(&User{Name: “pz”}) |
CreateInBatches | 分批创建 | db.Create(&[]User{ {Name: “pz”} }, 100) |
Save | 保存(会保存零值字段) | db.Save(&User{Name: “pz”}) |
First | 查询首条符合条件的数据(没查到报错) | db.First(&user) |
Take | 随便查询一条符合条件的数据(没查到报错) | db.Take(&user) |
Last | 查询最后一条符合条件的数据(没查到报错) | db.Last(&user) |
Find | 查询所有符合条件的数据(没查到报错) | db.Limit(1).Find(&user) |
FindInBatches | 批量查询并处理记录 | 详见《GORM - 高级查询》 |
FirstOrInit | 查询首条符合条件的数据,或者根据给定的条件初始化一个实例 | db.FirstOrInit(&user, User{Name: “non_existing”}) |
FirstOrCreate | 查询首条符合条件的数据,或者根据给定的条件创建一条新纪录 | db.FirstOrCreate(&user, User{Name: “non_existing”}) |
Update | 更新单字段 | db.Model(&User{}).Where(“name = ?”, “pz”).Update(User{Age: 18}) |
Updates | 更新多字段(不会更新零值字段,除非指定) | db.Model(&User{}).Where(“name = ?”, “pz”).Updates(User{Age: 18}) |
UpdateColumn | 更新单字段 | db.Model(&Foo{ID: 1}).UpdateColumn(“name”, “pz”) |
UpdateColumns | 更新多字段 | db.Model(&Foo{ID: 1}).UpdateColumns(Foo{}) |
Delete | 删除 | db.Delete(&email) |
Count | 计数 | db.Model(&User{}).Where(“name = ?”, “pz”).Count(&count) |
Row | 查询,返回值是 *sql.Row | db.Table(“users”).Where(“name = ?”, “pz”).Select(“name”, “age”).Row().Scan(&name, &age) |
Rows | 查询,返回值是 *sql.Rows | 详见《GORM - 高级查询》 |
Scan | 扫描,可以当 Find 用,也可以配合 *sql.Row 或 *sql.Rows 使用 |
db.Model(&User{}).Where(“name = ?”, “pz”).Scan(&users) |
Pluck | 查询单字段 | db.Model(&users).Pluck(“name”, &[]string]) |
ScanRows | 扫描 *sql.Rows 到结构体里 |
db.ScanRows(rows, &user) |
Connection | 连接数据库并执行方法(没用过,查不到资料) | |
Transaction | 开启事务,并在事务中执行 SQL | db.Transaction(func(tx *gorm.DB) error { // 先创建 user user := &User{Name: “pz”, Age: 18} res := tx.Create(user) if res.Error != nil { return res.Error } // 再更新 user res = tx.Model(user).Update(“age”, 20) return res.Error }) |
Begin | 开启事务 | db.Begin() |
Commit | 提交事务 | tx.Commit() |
Rollback | 回滚事务 | tx.Rollback() |
SavePoint | 创建保存点 | tx.SavePoint(“sp1”) |
RollbackTo | 回滚到保存点 | tx.RollbackTo(“sp1”) |
Exec | 直接执行 SQL | db.Exec(“DROP TABLE users”) |
GORM 预加载
GORM 有一个很好用的功能是预加载,下面举一个具体的例子。
有两张表:部门表(departments)和员工表(employees),员工表记录所在部门 ID(换句话说,部门和员工是 1:N 的关系)。
1 | type Department struct { |
如果现在需要查询,某部门的信息及其部门下所有员工信息,通常是通过 MySQL 中的 JOIN
语句查询。
GORM 提供了另一种查询方式,使用预加载(Preload)查询,代码如下:
1 | var departments []Department |
GORM 预加载在真正查询时,并不通过 JOIN
查询,而是 SELECT
查询两次:
- 查部门表
- 查员工表(WHERE group_id = xxx)
- 封装查询结果
使用 GORM 预加载,需要在声明模型时,在 Struct Tag 指定 foreignkey
和 association_foreignkey
(数据库并不需要真正建立外键,这里只用于 GORM 内部分析)。
GORM 指针传参
我在使用 GORM API 的时候,经常会遇到一个问题:传参应该传指针,还是应该不传指针?
1 | // 下面两种写法,哪种是合法的,还是都合法? |
报错报得多了之后,我决定把所有方法都试一遍,试完总结成两句话:
链式方法
尽量不传指针Finisher 方法
尽量传指针
链式方法
GORM 方法 | GORM 语句 | 没有指针是否合法 | 有指针是否合法 |
---|---|---|---|
Model | db.Model(User{} ).Find(&users) |
✅ | ✅ |
Table | db.Table("users" ).Find(&users) |
✅ | ❌ |
Where | db.Where(“id = ?”, 1 ).Find(&users) |
✅ | ✅ |
db.Where(User{Name: "pz"} ).Find(&users) |
✅ | ✅ | |
db.Where(“name IN ?”, []string{"pz"} ).Find(&users) |
✅ | ❌ | |
Select | db.Select("name" ).Find(&users) |
✅ | ❌ |
db.Select([]string{"name"} ).Find(&users) |
✅ | ❌ | |
Omit | db.Omit("name" ).Find(&users) |
✅ | ❌ |
Limit | db.Limit(1 ).Find(&users) |
✅ | ❌ |
Offset | db.Offset(1 ).Limit(1).Find(&users) |
✅ | ❌ |
Preload | db.Preload(clause.Associations ).Find(&users) |
✅ | ❌ |
Finisher 方法
GORM 方法 | GORM 语句 | 没有指针是否合法 | 有指针是否合法 |
---|---|---|---|
Create | db.Create(Foo{} ) |
❌ | ✅ |
db.Create([]Foo{ {} } ) |
✅ | ✅ | |
db.Create([1]Foo{ {} } ) |
❌ | ✅ | |
db.Model(&Foo{}).Create(map[string]interface{}{} ) |
✅ | ✅ | |
db.Model(&Foo{}).Create([]map[string]interface{}{} ) |
✅ | ✅ | |
CreateInBatches | db.CreateInBatches([]Foo{ {} } , 100) |
✅ | ✅ |
db.CreateInBatches([1]Foo{ {} }, 100) |
❌ | ✅ | |
Save | db.Save(Foo{} ) |
❌ | ✅ |
db.Save([]Foo{ {} } ) |
✅ | ✅ | |
db.Save([1]Foo{ {} } ) |
❌ | ✅ | |
db.Model(&Foo{ID: 1}).Save(map[string]interface{}{} ) |
✅ | ✅ | |
Update | db.Model(&Foo{ID: 1}).Update(“name”, "pz" ) |
✅ | ✅ |
Updates | db.Model(&Foo{ID: 1}).Updates(Foo{} ) |
✅ | ✅ |
db.Model(&Foo{ID: 1}).Updates(map[string]interface{}{} ) |
✅ | ✅ | |
UpdateColumn | db.Model(&Foo{ID: 1}).UpdateColumn(“name”, "pz" ) |
✅ | ✅ |
UpdateColumns | db.Model(&Foo{ID: 1}).UpdateColumns(Foo{} ) |
✅ | ✅ |
db.Model(&Foo{ID: 1}).UpdateColumns(map[string]interface{}{} ) |
✅ | ✅ | |
Find | db.Find(Foo ) |
❌ | ✅ |
db.Find(&Foo, 1 ) |
✅ | ✅ | |
db.Find(&Foo, []int{1, 2} ) |
✅ | ✅ | |
db.Find([]Foo ) |
❌ | ✅ | |
db.Find(&[]Foo, 1 ) |
✅ | ✅ | |
db.Find(&[]Foo, []int{1, 2} ) |
✅ | ✅ | |
First | db.First(Foo{} ) |
❌ | ✅ |
db.First(&Foo{}, 1 ) |
✅ | ✅ | |
Last | db.Last(Foo{} ) |
❌ | ✅ |
db.Last(&Foo{}, 1 ) |
✅ | ✅ | |
Take | db.Take(Foo{} ) |
❌ | ✅ |
db.Take(&Foo{}, 1 ) |
✅ | ✅ | |
Expr | db.Model(&Foo{ID: 1}).Update(“id”, gorm.Expr(“id + ?”, num )) |
✅ | ✅ |
一些 Tips
下面整理一些小 tips,可能会用得上。
查询
- First/Take/Last 查询不到数据时报错
record not found
,Find 查询不到数据时不报错
更新
- Updates 不会更新非零字段,Save 会更新非零字段
- Update/Updates 会更新 CreatedAt 字段,UpdateColumn/UpdateColumns 不会更新 CreatedAt 字段
- 即使 Select 没有指定 UpdatedAt 字段,实际上也会更新