目录
[1. 插入单条记录](#1. 插入单条记录)
[2. 批量插入](#2. 批量插入)
[3. 高级用法](#3. 高级用法)
[1. 删除的几种方式](#1. 删除的几种方式)
[2. 删除模式](#2. 删除模式)
[1. 软删除 (默认行为)](#1. 软删除 (默认行为))
[2. 物理删除 (永久删除)](#2. 物理删除 (永久删除))
[1. 使用 Struct 更新 (忽略零值)](#1. 使用 Struct 更新 (忽略零值))
[2. 使用 Map 更新 (包含所有值)](#2. 使用 Map 更新 (包含所有值))
[查询方法:First, Find, Last, Take](#查询方法:First, Find, Last, Take)
[1. 字符串条件](#1. 字符串条件)
[2. Struct & Map 条件](#2. Struct & Map 条件)
[3. 其他常用条件](#3. 其他常用条件)
[1. Belongs To (属于)](#1. Belongs To (属于))
[2. Has One (拥有一个)](#2. Has One (拥有一个))
[3. Has Many (拥有多个)](#3. Has Many (拥有多个))
[4. Many To Many (多对多)](#4. Many To Many (多对多))
[预加载 (Preload) - 解决 N+1 查询问题](#预加载 (Preload) - 解决 N+1 查询问题)
[关联模式 - 精细化操作](#关联模式 - 精细化操作)
[1. 基础用法:设置超时](#1. 基础用法:设置超时)
[2. 在 Web 中间件中使用](#2. 在 Web 中间件中使用)
[3. 在钩子函数中的使用](#3. 在钩子函数中的使用)
方式二:手动事务 (Begin/Commit/Rollback)
前言
ORM是一种编程技术,针对各类语言,ORM框架可以将数据库表和代码中的类(结构体)建立映射,从而支持开发者通过操作对象完成对数据库的CURD。

ORM的三大核心思想
-
数据库表和类(结构体)映射。
-
表中列字段和类(结构体)属性映射。
-
对象操作转换为SQL语句。
三大核心思想实际就是对应了数据库的三个核心模块:表结构、列结构、SQL
ORM框架的优点
-
避免手写SQL语句,提高开发效率。
-
规避了手写SQL语句容易出错的问题。
-
支持多种数据库,代码具有移植性。
ORM框架的缺点
-
ORM框架自动生成的SQL语句不如手写SQL高效。
-
存在学习成本,需要开发者了解对应ORM框架的使用规则。
其中Gorm库是go语言中一个非常强大的ORM(对象关系映射)库,它能够帮助我们很好的操作数据库和映射数据库表为go对象。
基本CRUD
插入数据
Create()方法用于将数据插入到数据库中。它既支持插入单条记录,也支持批量插入。
1. 插入单条记录
Go
user := User{Name: "Alice", Age: 30}
result := db.Create(&user)
2. 批量插入
Go
users := []User{
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
db.Create(&users)
3. 高级用法
-
指定字段插入 : 使用Select()方法只插入指定的字段。
Go// 只插入 Name 和 Age 字段,其他字段使用数据库默认值 db.Select("Name", "Age").Create(&user) -
忽略字段插入 : 使用Omit()方法忽略不想插入的字段。
Go// 插入时忽略 "Role" 字段 db.Omit("Role").Create(&user)
删除数据
1. 删除的几种方式
-
根据主键删除
Go// 删除主键为 10 的记录 db.Delete(&User{}, 10) // 批量删除主键为 1, 2, 3 的记录 db.Delete(&User{}, []int{1, 2, 3}) -
根据条件删除
Go// 删除所有 age < 18 的用户 db.Where("age < ?", 18).Delete(&User{}) -
删除已查询出的对象
Govar user User db.First(&user, 10) // 先查询出 ID 为 10 的用户 db.Delete(&user) // 再删除该对象
2. 删除模式
1. 软删除 (默认行为)
如果你的模型中嵌入了 gorm.Model (它包含 ID, CreatedAt, UpdatedAt, DeletedAt 字段),GORM 在执行 Delete 时并不会真正地从数据库中移除记录,而是将 **deleted_at**字段更新为当前时间。
-
普通查询会自动过滤 : 之后的常规查询(如
Find,First)会自动带上 **WHERE deleted_at IS NULL**条件,所以你看不到这条被删除的数据。 -
查询被软删除的数据 : 可以使用
Unscoped()方法来查询包含已软删除的记录。Govar user User // 查询所有 age=20 的用户,包括已软删除的 db.Unscoped().Where("age = ?", 20).Find(&user)
2. 物理删除 (永久删除)
如果你想真正地、永久地从数据库中删除一条记录,必须使用 **Unscoped()**方法。
Go
// 永久删除 ID 为 10 的用户
db.Unscoped().Delete(&User{}, 10)
更新数据
更新数据一般使用Updates()方法, 其接受两种类型的参数:结构体(struct)和映射(map),它们处理零值的方式截然不同。
1. 使用 Struct 更新 (忽略零值)
当你传入一个结构体时,GORM 默认只会更新其中的非零值字段。零值(如0, " ", false, nil)会被自动忽略,数据库中对应字段的值将保持不变。
Go
// 假设 user 的 ID 为 1
user := User{Name: "Bob", Age: 0, Active: false}
db.Model(&user).Updates(user)
// 生成的 SQL: UPDATE users SET name='Bob' WHERE id=1;
// 注意:Age 和 Active 字段因为是零值,所以没有被更新。
2. 使用 Map 更新 (包含所有值)
当你传入一个 map[string]interface{} 时,GORM 会更新 Map 中指定的所有键值对,包括零值。
Go
// 假设 user 的 ID 为 1
db.Model(&user).Updates(map[string]interface{}{
"name": "Bob",
"age": 0,
"active": false,
})
// 生成的 SQL: UPDATE users SET name='Bob', age=0, active=false WHERE id=1;
// 所有字段都被更新了,包括零值。
如果想要更新一个字段为零值,使用 map 是最直接的方式。同时更新也可以选中对应或者忽略相应字段进行更新:
- 使用Select()指定更新字段
Go
// 只更新 Name 字段,即使 Age 有值也会被忽略
db.Model(&user).Select("name").Updates(User{Name: "Bob", Age: 100})
// 生成的 SQL: UPDATE users SET name='Bob' WHERE id=1;
- 使用Omit()忽略更新字段
Go
// 更新除 Age 之外的所有非零值字段
db.Model(&user).Omit("age").Updates(User{Name: "Bob", Age: 100})
// 生成的 SQL: UPDATE users SET name='Bob' WHERE id=1;
查询数据
查询方法:First, Find, Last, Take
这四个是 GORM 中基础用法,用于执行查询并将结果填充到你的结构体或切片中。
| 方法 | 查询数量 | 默认排序 | 找不到记录时 |
|---|---|---|---|
First |
单条 | 按主键升序 (ORDER BY id ASC) |
返回 ErrRecordNotFound 错误 |
Last |
单条 | 按主键降序 (ORDER BY id DESC) |
返回 ErrRecordNotFound 错误 |
Take |
单条 | 无排序 | 返回 ErrRecordNotFound 错误 |
Find |
多条 (或单条) | 无排序 | 不返回错误,RowsAffected 为 0 |
使用示例:
Go
var user User
var users []User
// 1. First: 查询第一条记录
db.First(&user)
// SQL: SELECT * FROM users ORDER BY id LIMIT 1;
// 2. Last: 查询最后一条记录
db.Last(&user)
// SQL: SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 3. Take: 查询任意一条记录(不保证顺序,性能稍好)
db.Take(&user)
// SQL: SELECT * FROM users LIMIT 1;
// 4. Find: 查询所有记录
db.Find(&users)
// SQL: SELECT * FROM users;
// Find 也可以查询单条,但不会报错
db.Where("id = ?", 1).Find(&user)
在调用上述核心方法前,可以使用链式方法来构建复杂的查询条件。
1. 字符串条件
最接近原生 SQL 的方式,功能最强大。
Go
// 等于
db.Where("name = ?", "jinzhu").First(&user)
// IN 查询
db.Where("id IN ?", []int{1, 2, 3}).Find(&users)
// LIKE 模糊查询
db.Where("name LIKE ?", "%jin%").Find(&users)
// 范围查询
db.Where("age >= ? AND age < ?", 18, 30).Find(&users)
2. Struct & Map 条件
GORM 会自动将结构体或 Map 中非零值的字段作为查询条件。
Go
// Struct 查询
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SQL: SELECT * FROM users WHERE name = 'jinzhu' AND age = 20 LIMIT 1;
// Map 查询
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
注意 :如果结构体的主键字段有值,First、Find 等方法会自动将其作为查询条件。
3. 其他常用条件
Go
// Not 条件
db.Not("name", "jinzhu").Find(&users) // name != 'jinzhu'
// Or 条件
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
其中有一个 GORM 最容易遇到的问题。就是当使用一个已经包含主键值的结构体进行查询时,GORM 会自动将主键条件加入到 SQL 中,这可能导致查不到预期的数据。
错误示例:
Go
user := User{ID: 10} // 假设你想用这个 user 变量去查 ID=20 的数据
db.First(&user, 20) // 你以为会查 ID=20,但实际上...
// 生成的 SQL: SELECT * FROM users WHERE id = 10 ORDER BY id LIMIT 1;
// 因为 GORM 把 user.ID=10 也作为条件拼进去了!
正确做法:
-
清空结构体 :在复用结构体变量前,将其重置为零值。
Gouser = User{} db.First(&user, 20) -
使用
Where:显式指定条件,避免歧义。Godb.Where("id = ?", 20).First(&user)
其他查询方法
-
Count: 获取匹配的记录总数。Govar count int64 db.Model(&User{}).Where("active = ?", true).Count(&count) -
Pluck: 只查询指定的单个列,并填充到切片中。Govar names []string db.Model(&User{}).Where("active = ?", true).Pluck("name", &names) // SQL: SELECT name FROM users WHERE active = true; -
Select: 指定要查询的字段,常与Scan或Find配合使用。Govar results []User db.Select("name", "age").Where("id > ?", 10).Find(&results) -
Raw: 执行原生 SQL 查询,用于处理 GORM 无法直接表达的复杂查询。Govar results []User db.Raw("SELECT * FROM users WHERE name = ?", "jinzhu").Scan(&results)
总而言之,Find 是最通用的查询方法,而**First/Last/Take** 用于获取单条记录。配合**Where、Not、Or**等链式方法,可以构建出绝大多数业务所需的查询。
关联模式
GORM 的关联模式是其 ORM 能力的核心,它能够以声明式的方式定义模型之间的关系,并自动处理复杂的数据库操作。
GORM 将关联关系分为以下四种基本类型:
1. Belongs To (属于)
核心思想: 多的一方属于一的一方。外键定义在当前模型(拥有者)的表中。
典型场景: 订单(Order)属于用户(User),评论(Comment)属于文章(Article)。
模型定义:
Go
type User struct {
ID uint
Name string
}
type Order struct {
ID uint
UserID uint // 外键字段,默认命名规则是 "模型名 + ID"
User *User // 关联字段,指向 User 模型
Amount float64
}
在这个例子中,Order 模型 Belongs To User。orders 表中会有一个 user_id 外键。
2. Has One (拥有一个)
核心思想: 一的一方拥有一个一的关联模型。外键定义在关联模型(被拥有者)的表中。
典型场景: 用户(User)拥有一个个人资料(Profile),公民(Citizen)拥有一个身份证(IDCard)。
模型定义:
Go
type User struct {
ID uint
Name string
Profile Profile // 关联字段
}
type Profile struct {
ID uint
UserID uint // 外键字段,指向 User 模型的 ID
Bio string
}
User 模型 Has One Profile profiles 表中会有一个 user_id 外键。
3. Has Many (拥有多个)
核心思想: "一"的一方拥有多个"多"的关联模型。外键定义在关联模型(被拥有者)的表中。
典型场景: 用户(User)拥有多个订单(Order),文章(Article)拥有多个评论(Comment)。
模型定义:
Go
type User struct {
ID uint
Name string
Orders []Order // 关联字段,使用切片表示"多"
}
type Order struct {
ID uint
UserID uint // 外键字段,指向 User 模型的 ID
Amount float64
}
User 模型 Has Many Order。orders 表中会有一个 user_id 外键。
4. Many To Many (多对多)
核心思想: 两个模型互相拥有多个对方。这种关系需要一个中间连接表(Join Table)来实现。
典型场景: 用户(User)和角色(Role),文章(Article)和标签(Tag)。
模型定义:
Go
type User struct {
ID uint
Name string
Roles []Role `gorm:"many2many:user_roles;"` // 关联字段,指定中间表名
}
type Role struct {
ID uint
Name string
// 反向关联,中间表名必须一致
Users []User `gorm:"many2many:user_roles;"`
}
GORM 会自动创建并使用 user_roles 这个中间表,该表通常包含 user_id 和 role_id 两个外键。
关联操作的一些技巧
预加载 (Preload) - 解决 N+1 查询问题
当你查询主模型并访问其关联数据时,如果没有预加载,GORM 会为每一条关联数据单独发起一次数据库查询,这就是著名的N+1 查询问题,会严重影响性能。
反模式 (N+1 查询):
Go
var orders []Order
db.Find(&orders) // 1次查询
for _, order := range orders {
fmt.Println(order.User.Name) // 循环 N 次,触发 N 次查询
}
// 总共执行了 1 + N 次数据库查询
正模式 (使用 Preload):
Go
var orders []Order
// Preload 会使用 JOIN 或额外的 IN 查询,一次性加载所有关联数据
db.Preload("User").Find(&orders)
// 总共只执行 2 次数据库查询
关联模式 - 精细化操作
GORM 提供了Association() 方法,让你可以对关联数据进行更精细化的操作,如添加、删除、统计等。
Go
var user User
db.First(&user, 1)
// 1. 查找关联
var roles []Role
db.Model(&user).Association("Roles").Find(&roles)
// 2. 添加关联
role := Role{ID: 5, Name: "editor"}
db.Model(&user).Association("Roles").Append(&role)
// 3. 替换关联 (删除旧的,添加新的)
newRoles := []Role{{ID: 6, Name: "admin"}}
db.Model(&user).Association("Roles").Replace(newRoles)
// 4. 删除关联 (仅解除关系,不删除数据)
db.Model(&user).Association("Roles").Delete(&role)
// 5. 统计关联数量
var count int64
db.Model(&user).Association("Roles").Count(&count)
自定义外键和引用
GORM 的默认命名规则(如 UserID)很方便,但你也可以通过结构体标签gorm: "..."进行自定义。
Go
type Author struct {
ID uint
Name string
}
type Article struct {
ID uint
AuthorID uint // 自定义外键字段名
Author *Author `gorm:"foreignKey:AuthorID"` // 指定外键
Content string
}
你也可以自定义引用哪个字段,而不是默认的主键 ID。
Go
type User struct {
ID uint
Username string
Profile Profile `gorm:"references:Username"` // 引用 Username 字段
}
type Profile struct {
ID uint
UserName string // 外键字段,关联到 User.Username
Bio string
}
关联模式总结
表格
| 关联类型 | 关系描述 | 外键位置 |
|---|---|---|
| Belongs To | 当前模型属于另一个模型 | 当前模型表 |
| Has One | 当前模型拥有一个关联模型 | 关联模型表 |
| Has Many | 当前模型拥有多个关联模型 | 关联模型表 |
| Many To Many | 两个模型互相关联 | 中间连接表 |
上下文Context
Context(上下文) 是你控制查询生命周期(比如超时控制、链路追踪)的"遥控器";而 Transaction(事务) 则是保证数据一致性(要么全成功,要么全失败)的"安全网"。
在 Web 开发中,Context 非常重要。它主要用于:
- 控制超时:防止烂 SQL 把数据库连接池拖死。
- 传递追踪 ID:在日志中串联整个请求链路。
- 取消操作:如果用户取消了请求,数据库查询也应该立即停止
1. 基础用法:设置超时
GORM 支持通过 WithContext() 方法传递上下文。
这是最常见的场景。比如规定这个查询必须在 2 秒内完成,否则强制取消。
Go
import (
"context"
"time"
)
// 创建一个 2 秒超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 务必在操作结束后释放资源
var user User
// 将 ctx 传递给 GORM
err := db.WithContext(ctx).First(&user, 10).Error
if err != nil {
if err == context.DeadlineExceeded {
// 处理超时错误
fmt.Println("查询超时了!")
} else {
// 处理其他错误
fmt.Println("查询出错:", err)
}
}
2. 在 Web 中间件中使用
通常在 HTTP 请求进来时,把 Request 自带的 Context 传给 GORM。这样当客户端断开连接时,数据库查询也会自动终止。
Go
// 假设在 Gin 的 Handler 中
func GetUserHandler(c *gin.Context) {
var user User
// 直接使用 c.Request.Context()
// 这样如果用户浏览器关闭,这个查询也会立刻停止,不再浪费数据库资源
err := db.WithContext(c.Request.Context()).First(&user, 1).Error
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
}
3. 在钩子函数中的使用
我们也可以在 **BeforeCreate()**等钩子函数中获取 Context,用于记录日志或做权限校验。
Go
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
// 从 tx.Statement.Context 获取上下文
ctx := tx.Statement.Context
// 比如从上下文中取出 TraceID 记录到日志里
traceID := ctx.Value("trace_id")
fmt.Println("正在创建用户,TraceID:", traceID)
return nil
}
Transaction (事务)
事务的核心是 ACID 特性。在 GORM 中,处理事务主要有两种方式:推荐的标准方式(闭包)和手动方式。
方式一:标准事务 (推荐)
这是最安全、最简洁的写法。GORM 会自动帮你处理Commit(提交)和Rollback(回滚)。我们只需要关注业务逻辑,如果函数返回error,事务就会自动回滚。
场景:用户转账
A 给 B 转账 100 元。步骤:1. A 扣钱;2. B 加钱。这两步必须同时成功,或者同时失败。
Go
// db 是全局的 *gorm.DB 实例
func Transfer(tx *gorm.DB, fromID, toID uint, amount float64) error {
// 1. 开启事务 (使用闭包方式)
return tx.Transaction(func(tx *gorm.DB) error {
var fromUser, toUser User
// 2. 锁定并查询转出账户 (使用 tx 而不是 db!)
// ForUpdate 是数据库层面的行锁,防止并发扣款
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&fromUser, fromID).Error; err != nil {
return err // 返回 error,自动回滚
}
// 3. 检查余额
if fromUser.Balance < amount {
return errors.New("余额不足") // 返回 error,自动回滚
}
// 4. 扣款
if err := tx.Model(&fromUser).Update("balance", fromUser.Balance - amount).Error; err != nil {
return err
}
// 5. 加款
if err := tx.Model(&toUser).First(&toUser, toID).Update("balance", toUser.Balance + amount).Error; err != nil {
return err
}
// 6. 没有任何错误返回,自动提交事务
return nil
})
}
关键点:
- 使用
tx:在闭包内部,必须使用传入的 tx 对象执行操作,不能用全局的db,否则操作不会在同一个事务里。 - 自动回滚:只要return error,GORM 就会帮我们回滚。
方式二:手动事务 (Begin/Commit/Rollback)
这种方式更灵活,但也更繁琐,容易忘记回滚。通常用于复杂的逻辑控制,或者需要在事务中调用其他函数时。
Go
func ManualTransaction() error {
// 1. 开始事务
tx := db.Begin()
// 2. 必须用 defer 保证异常发生时能回滚
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 3. 执行操作 (记得用 tx)
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
tx.Rollback() // 出错手动回滚
return err
}
// ... 更多操作 ...
// 4. 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback() // 提交失败也要回滚
return err
}
return nil
}
进阶:嵌套事务 (SavePoint)
GORM 支持嵌套事务。外层事务开启后,内层事务会创建一个保存点(SavePoint)。如果内层失败,可以只回滚到保存点,而不影响外层已经成功的操作。
Go
db.Transaction(func(tx *gorm.DB) error {
// --- 外层事务 ---
tx.Create(&User{Name: "OuterUser"})
// --- 内层事务 ---
// 如果这里出错,只会回滚 InnerUser,OuterUser 依然存在
return tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&User{Name: "InnerUser"})
// return errors.New("模拟内层失败") // 如果取消注释,InnerUser 会被回滚
return nil
})
})
这两个模块配合使用效果更佳:比如在事务中传入带有超时控制的 Context,既保证了数据安全,又防止了死锁导致的长时间阻塞。