GORM 框架完全指南
GORM 框架完全指南
前言
GORM 是 Go 语言最流行的 ORM 框架。功能包括 CRUD、关联关系、事务、钩子、预加载、自动迁移、泛型 API 等。因为官方文章以及说得很详细的了,因此本文主要以代码方式梳理记录 GORM 的核心用法,可作为日后开发速查手册。
目录
1. 安装与连接
// 安装(在项目根目录执行)
// go get -u gorm.io/gorm ← GORM 核心库
// go get -u gorm.io/driver/mysql ← MySQL 驱动
// go get -u gorm.io/driver/postgres ← PostgreSQL 驱动
// go get -u gorm.io/driver/sqlite ← SQLite 驱动(纯 Go,无需 CGO)
package main
import (
"time"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func main() {
// ===================== MySQL 连接 =====================
// DSN 格式: 用户名:密码@tcp(主机:端口)/数据库名?参数
// charset=utf8mb4 → 支持 emoji 等 4 字节 Unicode
// parseTime=True → 将 MySQL 的时间类型解析为 Go 的 time.Time(必须开启)
// loc=Local → 使用本地时区
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// ===================== PostgreSQL 连接 =====================
// sslmode=disable → 本地开发关闭 SSL,生产环境应改为 require
dsn2 := "host=localhost user=postgres password=secret dbname=mydb port=5432 sslmode=disable"
db, err = gorm.Open(postgres.Open(dsn2), &gorm.Config{})
// ===================== SQLite 连接 =====================
// 文件路径,如果文件不存在会自动创建
db, err = gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// ===================== 连接池配置 =====================
// GORM 底层使用 database/sql 的连接池,通过 db.DB() 获取 *sql.DB 进行配置
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数(连接用完后保留多少个不关闭,等待复用)
sqlDB.SetMaxOpenConns(100) // 最大打开连接数(同时能有多少个连接在用)
sqlDB.SetConnMaxLifetime(time.Hour) // 单个连接最大存活时间(超过后被关闭重建,防止 DB 端超时断开)
}
2. 模型定义
import "gorm.io/gorm"
// ===================== gorm.Model 内置字段 =====================
// GORM 提供了一个内置的 gorm.Model 结构体,包含以下 4 个字段:
// type Model struct {
// ID uint `gorm:"primarykey"` // 主键,自增
// CreatedAt time.Time // 创建时间,Create 时自动填充
// UpdatedAt time.Time // 更新时间,每次 Save/Update 自动更新
// DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除时间,调用 Delete 时填充,不会真正删除记录
// }
// 嵌入 gorm.Model 就自动拥有这 4 个字段,不需要自己写
// ===================== 基本模型示例 =====================
type User struct {
gorm.Model // 内嵌,自动获得 ID, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"type:varchar(100);not null;index"`
// type:varchar(100) → 数据库列类型为 VARCHAR(100)
// not null → 不允许 NULL
// index → 为此字段创建普通索引
Email string `gorm:"uniqueIndex;size:128"`
// uniqueIndex → 创建唯一索引(不允许重复值)
// size:128 → 等同于 type:varchar(128)
Age int `gorm:"default:18"`
// default:18 → 数据库默认值为 18(插入时如果没赋值就用 18)
Active bool `gorm:"default:true"`
Profile Profile // Has One 关联:一个 User 拥有一个 Profile
Orders []Order // Has Many 关联:一个 User 拥有多个 Order
}
// ===================== 自定义表名 =====================
// 默认 GORM 会把结构体名转为 snake_case 复数:User → users
// 如果想自定义,实现 TableName() 方法
func (User) TableName() string {
return "t_users" // 表名变为 t_users
}
// ===================== 常用 struct tag 详解 =====================
type Product struct {
ID uint `gorm:"primaryKey;autoIncrement"`
// primaryKey → 主键
// autoIncrement → 自增(MySQL 默认主键就自增,这里是显式声明)
Code string `gorm:"type:varchar(50);uniqueIndex;not null;comment:产品编码"`
// comment:产品编码 → 数据库列注释(DDL 里的 COMMENT '产品编码')
Name string `gorm:"size:200;not null"`
// size:200 → varchar(200)
Price float64 `gorm:"type:decimal(10,2);default:0"`
// decimal(10,2) → 总共 10 位数字,小数点后 2 位
Stock int `gorm:"check:stock >= 0"`
// check:stock >= 0 → 数据库 CHECK 约束,stock 不能小于 0
Category string `gorm:"index:idx_cat_price"`
SalePrice float64 `gorm:"index:idx_cat_price"`
// 两个字段用相同的索引名 idx_cat_price → 组成复合索引
// 等价于 CREATE INDEX idx_cat_price ON products(category, sale_price)
Remark string `gorm:"type:text"`
// text → MySQL TEXT 类型(长文本)
IgnoreMe string `gorm:"-"`
// "-" → GORM 完全忽略此字段(不建表、不读写)
ReadOnly string `gorm:"->"`
// "->" → 只读:只从 DB 读取,写入时忽略
WriteOnly string `gorm:"<-"`
// "<-" → 只写:创建和更新都可写(但不影响读取,读取也能读到)
CreateOnly string `gorm:"<-:create"`
// "<-:create" → 仅在 Create 时可写,Update 时忽略
UpdateOnly string `gorm:"<-:update"`
// "<-:update" → 仅在 Update 时可写,Create 时忽略
}
// ===================== JSON 标签配合使用 =====================
// gorm tag 控制数据库行为,json tag 控制 JSON 序列化行为
type APIUser struct {
gorm.Model
Name string `gorm:"size:100" json:"name"` // DB 列 varchar(100),JSON key 为 "name"
Email string `gorm:"size:128" json:"email,omitempty"` // omitempty: JSON 序列化时空值不输出
}
// ===================== 复合主键 =====================
// 多个字段共同组成主键
type UserLanguage struct {
UserID uint `gorm:"primaryKey"` // 联合主键的一部分
LanguageID uint `gorm:"primaryKey"` // 联合主键的另一部分
Skill string
// 建表:PRIMARY KEY (user_id, language_id)
}
3. 自动迁移
// AutoMigrate 会根据结构体定义自动:
// ✅ 创建不存在的表
// ✅ 添加缺失的列
// ✅ 添加缺失的索引
// ❌ 不会删除多余的列(防止误删数据)
// ❌ 不会修改已有列的类型(防止数据丢失)
// ❌ 不会删除已有索引
// 因此 AutoMigrate 适合开发阶段,生产环境推荐用 golang-migrate / goose 等专用迁移工具
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 依次检查 users、products、orders 三张表,不存在就建,缺字段就加
// ===================== 判断表是否存在 =====================
db.Migrator().HasTable(&User{}) // 通过结构体判断(会调用 TableName())
db.Migrator().HasTable("users") // 通过表名字符串判断
// ===================== 手动建表/删表 =====================
db.Migrator().CreateTable(&User{}) // CREATE TABLE users (...)
db.Migrator().DropTable(&User{}) // DROP TABLE users
db.Migrator().DropTable("users") // 也可以传表名
// ===================== 列操作 =====================
db.Migrator().AddColumn(&User{}, "Age") // ALTER TABLE users ADD COLUMN age ...
db.Migrator().DropColumn(&User{}, "Age") // ALTER TABLE users DROP COLUMN age
db.Migrator().AlterColumn(&User{}, "Name") // ALTER TABLE users MODIFY COLUMN name ...(按最新 struct tag)
db.Migrator().HasColumn(&User{}, "Name") // 判断列是否存在 → true/false
// ===================== 索引操作 =====================
db.Migrator().CreateIndex(&User{}, "Name") // 为 Name 字段创建索引
db.Migrator().DropIndex(&User{}, "idx_name") // 删除索引
db.Migrator().HasIndex(&User{}, "idx_name") // 判断索引是否存在
// ===================== 重命名 =====================
db.Migrator().RenameTable("users", "t_users") // 重命名表
db.Migrator().RenameColumn(&User{}, "Name", "FullName") // 重命名列
4. 创建(Create)
// ===================== 创建单条记录 =====================
user := User{Name: "Alice", Age: 25, Email: "alice@go.dev"}
result := db.Create(&user)
// 生成 SQL: INSERT INTO users (name, age, email, created_at, updated_at) VALUES ('Alice', 25, 'alice@go.dev', '2024-...', '2024-...')
// 执行后:
// user.ID → 自动回填数据库生成的主键(比如 1)
// user.CreatedAt → 自动回填创建时间
// result.Error → 如果插入失败,这里是 error;成功则为 nil
// result.RowsAffected → 插入了几行(通常为 1)
// ===================== 指定字段创建 =====================
// 只插入 Name 和 Age,其他字段即使有值也不会插入(使用数据库默认值或零值)
db.Select("Name", "Age").Create(&user)
// 生成 SQL: INSERT INTO users (name, age, created_at, updated_at) VALUES ('Alice', 25, ...)
// Email 字段被忽略,即使 user.Email 有值
// ===================== 忽略字段创建 =====================
// 除 Age 外的所有字段都插入
db.Omit("Age").Create(&user)
// 生成 SQL: INSERT INTO users (name, email, created_at, updated_at) VALUES ('Alice', 'alice@go.dev', ...)
// Age 被忽略,使用数据库默认值(如果 gorm tag 里写了 default:18 就是 18)
// ===================== 批量创建 =====================
users := []User{
{Name: "Bob", Age: 20},
{Name: "Carol", Age: 22},
{Name: "Dave", Age: 30},
}
db.Create(&users)
// 生成 SQL: INSERT INTO users (name, age, ...) VALUES ('Bob',20,...), ('Carol',22,...), ('Dave',30,...)
// 一条 SQL 插入多行
// 执行后 users[0].ID, users[1].ID, users[2].ID 都会回填
// ===================== 分批插入 =====================
// 如果 users 有 10000 条,一次性 INSERT 可能超过 MySQL 的 max_allowed_packet
// CreateInBatches 会自动分成多条 SQL,每条最多插入 100 行
db.CreateInBatches(&users, 100)
// 生成: 第 1 条 INSERT 100 行,第 2 条 INSERT 100 行 ... 直到全部插完
// ===================== 用 Map 创建 =====================
// 不基于结构体,直接用 map[string]interface{} 指定列名和值
// ⚠️ 不会触发 Hook(BeforeCreate 等)
// ⚠️ 不会回填主键
// ⚠️ 不会自动处理关联
db.Model(&User{}).Create(map[string]interface{}{
"Name": "Eve", "Age": 28,
})
// 生成 SQL: INSERT INTO users (name, age) VALUES ('Eve', 28)
// 批量 Map 创建
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "Frank", "Age": 30},
{"Name": "Grace", "Age": 26},
})
// 生成 SQL: INSERT INTO users (name, age) VALUES ('Frank', 30), ('Grace', 26)
// ===================== Upsert(插入或更新)=====================
import "gorm.io/gorm/clause"
// 场景:插入时如果 email 已存在(唯一索引冲突),则更新 name 和 age
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}}, // 冲突判断列
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), // 冲突时更新哪些列
}).Create(&users)
// MySQL 生成: INSERT INTO users (...) VALUES (...) ON DUPLICATE KEY UPDATE name=VALUES(name), age=VALUES(age)
// PostgreSQL 生成: INSERT INTO users (...) VALUES (...) ON CONFLICT (email) DO UPDATE SET name=EXCLUDED.name, age=EXCLUDED.age
// 冲突时什么都不做(忽略重复记录)
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// MySQL 生成: INSERT IGNORE INTO users (...) VALUES (...)
// PostgreSQL 生成: INSERT INTO users (...) VALUES (...) ON CONFLICT DO NOTHING
5. 查询(Query)
var user User
var users []User
// =============================================
// 基础查询
// =============================================
// --- 主键查询 ---
db.First(&user, 1)
// 生成 SQL: SELECT * FROM users WHERE id = 1 ORDER BY id LIMIT 1
// First 会按主键升序排序并取第一条
// ⚠️ 如果找不到记录,返回 gorm.ErrRecordNotFound
db.First(&user, "id = ?", 10)
// 生成 SQL: SELECT * FROM users WHERE id = 10 ORDER BY id LIMIT 1
// 和上面等价,只是用字符串条件写法
// 多主键查询
db.Find(&users, []int{1, 2, 3})
// 生成 SQL: SELECT * FROM users WHERE id IN (1, 2, 3)
// --- Take:取一条记录(不指定排序,由数据库决定顺序)---
db.Take(&user)
// 生成 SQL: SELECT * FROM users LIMIT 1
// 和 First 的区别:First 会加 ORDER BY id,Take 不加排序
// --- Last:取最后一条 ---
db.Last(&user)
// 生成 SQL: SELECT * FROM users ORDER BY id DESC LIMIT 1
// 按主键降序取第一条,即"最后一条"
// --- 查询全部 ---
db.Find(&users)
// 生成 SQL: SELECT * FROM users
// ⚠️ Find 找不到记录时不会报错,只是 users 为空切片,result.RowsAffected = 0
// =============================================
// Where 条件
// =============================================
// --- 字符串条件 ---
db.Where("name = ?", "Alice").First(&user)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice' ORDER BY id LIMIT 1
db.Where("name <> ?", "Alice").Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name <> 'Alice'
// <> 等同于 !=
db.Where("name IN ?", []string{"Alice", "Bob"}).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name IN ('Alice', 'Bob')
db.Where("name LIKE ?", "%ali%").Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name LIKE '%ali%'
// 匹配任何包含 "ali" 的名字(不区分大小写取决于 DB 配置)
db.Where("age BETWEEN ? AND ?", 20, 30).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE age BETWEEN 20 AND 30
// 包含边界:age >= 20 AND age <= 30
db.Where("created_at > ?", time.Now().AddDate(0, 0, -7)).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE created_at > '2024-03-08 ...'
// 查询最近 7 天创建的记录
// 多个 Where 会用 AND 连接
db.Where("name = ?", "Alice").Where("age > ?", 18).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice' AND age > 18
// --- Struct 条件 ---
// ⚠️ 重要陷阱:零值字段会被自动忽略!
db.Where(&User{Name: "Alice", Age: 0}).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice'
// Age = 0 是 int 的零值,被 GORM 认为"没有赋值",所以被忽略
// 如果你确实想查 age = 0 的记录,不要用 struct 条件,用 Map 或字符串
db.Where(&User{Name: "Alice", Age: 25}).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice' AND age = 25
// Age = 25 不是零值,所以正常参与查询
// --- Map 条件 ---
// Map 不存在"零值被忽略"的问题,所有 key 都会参与查询
db.Where(map[string]interface{}{"name": "Alice", "age": 0}).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice' AND age = 0
// Age = 0 也会参与查询条件
// =============================================
// Select / 排序 / 分页
// =============================================
// --- Select 指定查询列 ---
db.Select("name", "age").Find(&users)
// 生成 SQL: SELECT name, age FROM users
// 只查 name 和 age 两列,其他字段为零值
db.Select("COALESCE(age, 0) as age").Find(&users)
// 生成 SQL: SELECT COALESCE(age, 0) as age FROM users
// COALESCE:如果 age 是 NULL 就返回 0
// --- 排序 ---
db.Order("age desc, name asc").Find(&users)
// 生成 SQL: SELECT * FROM users ORDER BY age DESC, name ASC
// 先按 age 降序,age 相同的再按 name 升序
db.Order("age desc").Order("name").Find(&users)
// 生成 SQL: SELECT * FROM users ORDER BY age DESC, name
// 多次 Order 调用会拼接
// --- 分页 ---
db.Offset(10).Limit(20).Find(&users)
// 生成 SQL: SELECT * FROM users LIMIT 20 OFFSET 10
// 跳过前 10 条,取接下来的 20 条(第 11~30 条)
// 常见用法:page=2, pageSize=20 → Offset((2-1)*20).Limit(20)
// 取消 Limit(传 -1)
db.Offset(10).Limit(-1).Find(&users)
// 生成 SQL: SELECT * FROM users OFFSET 10
// 从第 11 条开始取到末尾
// --- Distinct ---
db.Distinct("name").Find(&users)
// 生成 SQL: SELECT DISTINCT name FROM users
// 去重查询 name
// --- Count ---
var count int64
db.Model(&User{}).Where("age > ?", 18).Count(&count)
// 生成 SQL: SELECT COUNT(*) FROM users WHERE age > 18
// count 的值就是满足条件的记录数
// ⚠️ Count 需要指定 Model 或 Table,否则 GORM 不知道查哪张表
// --- Pluck:查询单列到切片 ---
var names []string
db.Model(&User{}).Pluck("name", &names)
// 生成 SQL: SELECT name FROM users
// names = ["Alice", "Bob", "Carol", ...]
// Pluck 只能查一列,结果直接存到基础类型切片
var ages []int
db.Model(&User{}).Pluck("age", &ages)
// 生成 SQL: SELECT age FROM users
// =============================================
// Not / Or
// =============================================
db.Not("name = ?", "Alice").Find(&users)
// 生成 SQL: SELECT * FROM users WHERE NOT (name = 'Alice')
// 等价于 name <> 'Alice'
db.Not(map[string]interface{}{"name": []string{"Alice", "Bob"}}).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name NOT IN ('Alice', 'Bob')
db.Where("age > ?", 18).Or("name = ?", "Admin").Find(&users)
// 生成 SQL: SELECT * FROM users WHERE age > 18 OR name = 'Admin'
// 查询 age > 18 的用户,或者 name 为 Admin 的用户(Admin 不受 age 限制)
// 更复杂的 Or 组合
db.Where("age > ? AND active = ?", 18, true).Or("name = ?", "Admin").Find(&users)
// 生成 SQL: SELECT * FROM users WHERE (age > 18 AND active = true) OR name = 'Admin'
// =============================================
// FirstOrInit / FirstOrCreate
// =============================================
// --- FirstOrInit ---
// 先尝试查找,找到就返回;找不到就在内存中用条件 + Attrs 初始化(不写入数据库)
db.Where(User{Name: "new_user"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// 第 1 步:执行查询
// 生成 SQL: SELECT * FROM users WHERE name = 'new_user' ORDER BY id LIMIT 1
// 第 2 步:
// 如果找到了 → user = 数据库中的记录(忽略 Attrs)
// 如果没找到 → user = User{Name: "new_user", Age: 20}
// ✅ Name 来自 Where 条件
// ✅ Age 来自 Attrs
// ⚠️ 此时 user 只在内存中,数据库里没有这条记录(ID = 0)
// 如果没找到且不使用 Attrs:
db.Where(User{Name: "new_user"}).FirstOrInit(&user)
// 没找到 → user = User{Name: "new_user", Age: 0}
// 只有 Where 条件里的字段被赋值
// --- FirstOrCreate ---
// 先尝试查找,找到就返回;找不到就创建并写入数据库
db.Where(User{Name: "new_user"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// 第 1 步:执行查询
// 生成 SQL: SELECT * FROM users WHERE name = 'new_user' ORDER BY id LIMIT 1
// 第 2 步:
// 如果找到了 → user = 数据库中的记录(Attrs 被忽略,不修改已有记录)
// 如果没找到 → 执行 INSERT:
// 生成 SQL: INSERT INTO users (name, age, created_at, updated_at) VALUES ('new_user', 20, ...)
// user = User{ID: <新生成>, Name: "new_user", Age: 20, CreatedAt: ..., UpdatedAt: ...}
// ✅ Name 来自 Where 条件
// ✅ Age 来自 Attrs
// ✅ 记录已写入数据库
// --- Assign ---
// 无论是否找到,都会将 Assign 的值赋给 user
// FirstOrInit + Assign:赋值但不写库
db.Where(User{Name: "new_user"}).Assign(User{Age: 30}).FirstOrInit(&user)
// 找到了 → user = 数据库记录,但 user.Age 被覆盖为 30(仅内存中改,DB 不变)
// 没找到 → user = User{Name: "new_user", Age: 30}(仅内存)
// FirstOrCreate + Assign:赋值且写库
db.Where(User{Name: "new_user"}).Assign(User{Age: 30}).FirstOrCreate(&user)
// 找到了 → 执行 UPDATE,将 Age 更新为 30
// 生成 SQL: UPDATE users SET age = 30, updated_at = '...' WHERE id = <找到的ID>
// 没找到 → 执行 INSERT,Name = "new_user", Age = 30
// 生成 SQL: INSERT INTO users (name, age, ...) VALUES ('new_user', 30, ...)
// Attrs vs Assign 总结:
// Attrs: 仅在"创建"时生效,找到已有记录则忽略
// Assign: 无论"创建"还是"找到"都生效
6. 高级查询
// =============================================
// 子查询
// =============================================
// 先构造子查询(不会立即执行)
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "A%").Table("users")
// subQuery 对应: SELECT AVG(age) FROM users WHERE name LIKE 'A%'
db.Where("age > (?)", subQuery).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE age > (SELECT AVG(age) FROM users WHERE name LIKE 'A%')
// 查询年龄大于"所有 A 开头用户的平均年龄"的用户
// FROM 子查询:把子查询结果当作临时表
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age > ?", 18).Find(&users)
// 生成 SQL: SELECT * FROM (SELECT name, age FROM users) as u WHERE age > 18
// =============================================
// Group / Having
// =============================================
type Result struct {
Date string
Total int64
}
var results []Result
db.Model(&Order{}).
Select("DATE(created_at) as date, SUM(amount) as total").
Group("DATE(created_at)").
Having("SUM(amount) > ?", 100).
Scan(&results)
// 生成 SQL:
// SELECT DATE(created_at) as date, SUM(amount) as total
// FROM orders
// GROUP BY DATE(created_at)
// HAVING SUM(amount) > 100
// 按日期分组统计订单总额,只返回总额 > 100 的日期
// 结果存到自定义的 Result 结构体(用 Scan 而非 Find,因为 Result 不对应任何表)
// =============================================
// Joins
// =============================================
type UserWithOrder struct {
UserName string
OrderTotal float64
}
var joinResults []UserWithOrder
db.Model(&User{}).
Select("users.name as user_name, SUM(orders.amount) as order_total").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Group("users.id").
Scan(&joinResults)
// 生成 SQL:
// SELECT users.name as user_name, SUM(orders.amount) as order_total
// FROM users
// LEFT JOIN orders ON orders.user_id = users.id
// GROUP BY users.id
// 查询每个用户的订单总额(LEFT JOIN:没有订单的用户也会出现,order_total 为 NULL)
// 带条件的 Joins
db.Model(&User{}).
Joins("INNER JOIN orders ON orders.user_id = users.id AND orders.amount > ?", 100).
Find(&users)
// 生成 SQL:
// SELECT users.* FROM users
// INNER JOIN orders ON orders.user_id = users.id AND orders.amount > 100
// INNER JOIN:只返回有 amount > 100 订单的用户
// =============================================
// Scan 到自定义结构体
// =============================================
// 当查询结果不直接对应某个 Model 时,用 Scan 代替 Find
type SimpleUser struct {
Name string
Age int
}
var simpleUsers []SimpleUser
db.Model(&User{}).Select("name", "age").Scan(&simpleUsers)
// 生成 SQL: SELECT name, age FROM users
// 结果映射到 SimpleUser(只有 Name 和 Age 两个字段)
// =============================================
// FindInBatches:分批处理大量数据
// =============================================
// 场景:需要处理百万级记录,一次全加载到内存会 OOM
// FindInBatches 每次只查 100 条,处理完再查下一批
db.Where("active = ?", true).FindInBatches(&users, 100, func(tx *gorm.DB, batch int) error {
// batch 是第几批(从 1 开始)
// users 是当前批次的数据(最多 100 条)
// tx 是当前批次的 DB 对象(可以用来做 Count 等)
for _, user := range users {
// 处理每条记录,比如发邮件、同步数据等
_ = user
}
fmt.Printf("第 %d 批处理了 %d 条\n", batch, tx.RowsAffected)
// 返回 error 会停止后续批次
// 返回 nil 继续处理下一批
return nil
})
// 生成 SQL:
// 第 1 批: SELECT * FROM users WHERE active = true LIMIT 100
// 第 2 批: SELECT * FROM users WHERE active = true LIMIT 100 OFFSET 100
// 第 3 批: SELECT * FROM users WHERE active = true LIMIT 100 OFFSET 200
// ... 直到没有更多数据
// =============================================
// Locking(锁)
// =============================================
import "gorm.io/gorm/clause"
// 悲观锁 - 排他锁(写锁)
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// 生成 SQL: SELECT * FROM users FOR UPDATE
// 其他事务不能读也不能写这些行,直到当前事务提交
// 悲观锁 - 共享锁(读锁)
db.Clauses(clause.Locking{Strength: "SHARE"}).Find(&users)
// 生成 SQL: SELECT * FROM users FOR SHARE
// 其他事务可以读但不能写这些行
// =============================================
// Optimizer / Index Hints
// =============================================
import "gorm.io/hints"
// 建议数据库使用某个索引
db.Clauses(hints.UseIndex("idx_name")).Find(&users)
// 生成 SQL: SELECT * FROM users USE INDEX (idx_name)
// 强制使用某个索引
db.Clauses(hints.ForceIndex("idx_name")).Where("name = ?", "Alice").Find(&users)
// 生成 SQL: SELECT * FROM users FORCE INDEX (idx_name) WHERE name = 'Alice'
// =============================================
// 命名参数
// =============================================
import "database/sql"
db.Where("name = @name OR age = @age", sql.Named("name", "Alice"), sql.Named("age", 18)).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice' OR age = 18
// 用 @name、@age 替代 ?,更清晰,适合条件很多的场景
// =============================================
// Map 结果
// =============================================
// 不想定义结构体,直接用 map 接收结果
var resultMap []map[string]interface{}
db.Model(&User{}).Find(&resultMap)
// 生成 SQL: SELECT * FROM users
// resultMap 类似: [{"id": 1, "name": "Alice", "age": 25}, {"id": 2, ...}, ...]
// 每条记录是一个 map,key 是列名,value 是 interface{}
7. 更新(Update)
// 假设已有: user = User{ID: 1, Name: "Alice", Age: 25, Email: "alice@go.dev"}
// =============================================
// 单字段更新
// =============================================
db.Model(&user).Update("name", "new_name")
// 生成 SQL: UPDATE users SET name = 'new_name', updated_at = '2024-...' WHERE id = 1
// ⚠️ 必须通过 Model 指定是哪条记录(user.ID = 1 → WHERE id = 1)
// ⚠️ updated_at 自动更新
// 条件更新(不依赖主键,用 Where 指定条件)
db.Model(&User{}).Where("age < ?", 18).Update("active", false)
// 生成 SQL: UPDATE users SET active = false, updated_at = '...' WHERE age < 18
// 将所有 age < 18 的用户设为 inactive
// =============================================
// 多字段更新(Struct)
// =============================================
// ⚠️ 重要陷阱:struct 只更新非零值字段!零值字段(0、""、false)会被忽略
db.Model(&user).Updates(User{Name: "hello", Age: 0})
// 生成 SQL: UPDATE users SET name = 'hello', updated_at = '...' WHERE id = 1
// ❌ Age = 0 是 int 零值,被 GORM 忽略,不会更新!
// 如果你想把 Age 更新为 0,不要用 struct,用 Map 或 Select
db.Model(&user).Updates(User{Name: "hello", Age: 30})
// 生成 SQL: UPDATE users SET name = 'hello', age = 30, updated_at = '...' WHERE id = 1
// Age = 30 不是零值,正常更新
// =============================================
// 多字段更新(Map)
// =============================================
// Map 可以更新零值,不存在"忽略"问题
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 0})
// 生成 SQL: UPDATE users SET name = 'hello', age = 0, updated_at = '...' WHERE id = 1
// ✅ name 和 age 都会更新,包括 age = 0
// =============================================
// Select / Omit 控制更新字段
// =============================================
// Select:只更新指定的字段(即使是零值也会更新)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new", Age: 0})
// 生成 SQL: UPDATE users SET name = 'new', age = 0, updated_at = '...' WHERE id = 1
// ✅ 强制更新 Name 和 Age,即使 Age = 0
// Select("*"):更新所有字段(包括零值),效果类似 Save
db.Model(&user).Select("*").Updates(User{Name: "new"})
// 生成 SQL: UPDATE users SET name='new', age=0, email='', active=false, updated_at='...' WHERE id = 1
// ⚠️ 所有未赋值的字段都会被更新为零值!慎用
// Omit:跳过指定字段,其余非零值字段都更新
db.Model(&user).Omit("Name").Updates(User{Name: "ignored", Age: 30})
// 生成 SQL: UPDATE users SET age = 30, updated_at = '...' WHERE id = 1
// Name 被忽略,即使传了值
// =============================================
// SQL 表达式
// =============================================
// 在现有值的基础上做计算(不是直接赋值)
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 10))
// 生成 SQL: UPDATE products SET price = price * 2 + 10, updated_at = '...' WHERE id = 1
// 原来 price = 50,执行后 price = 50 * 2 + 10 = 110
db.Model(&product).Updates(map[string]interface{}{
"price": gorm.Expr("price + ?", 5), // price = price + 5
"stock": gorm.Expr("stock - ?", 1), // stock = stock - 1
})
// 生成 SQL: UPDATE products SET price = price + 5, stock = stock - 1, updated_at = '...' WHERE id = 1
// 适合"扣库存"、"加积分"等场景,避免先 SELECT 再 UPDATE 的并发问题
// =============================================
// 批量更新
// =============================================
db.Model(&User{}).Where("active = ?", false).Updates(map[string]interface{}{"deleted": true})
// 生成 SQL: UPDATE users SET deleted = true, updated_at = '...' WHERE active = false
// 把所有 active=false 的用户标记为 deleted
// =============================================
// Save:全量保存所有字段(包括零值)
// =============================================
user.Name = "" // 空字符串
user.Age = 0 // 零值
db.Save(&user)
// 生成 SQL: UPDATE users SET name='', age=0, email='alice@go.dev', active=true, updated_at='...' WHERE id=1
// ⚠️ Save 会更新所有字段(包括零值),相当于 struct 的完整覆盖
// ⚠️ 如果 user.ID = 0(没有主键),Save 会执行 INSERT 而非 UPDATE
// 建议:除非确定要更新所有字段,否则用 Updates 更安全
// =============================================
// 不触发 Hook 和时间追踪的更新
// =============================================
// UpdateColumn / UpdateColumns 不会:
// ❌ 触发 BeforeUpdate / AfterUpdate 钩子
// ❌ 自动更新 updated_at
// 适合纯粹的数据修正、数据迁移等场景
db.Model(&User{}).Where("id = ?", 1).UpdateColumn("name", "silent")
// 生成 SQL: UPDATE users SET name = 'silent' WHERE id = 1
// 注意没有 updated_at = '...'
db.Model(&User{}).Where("id = ?", 1).UpdateColumns(map[string]interface{}{"name": "silent", "age": 30})
// 生成 SQL: UPDATE users SET name = 'silent', age = 30 WHERE id = 1
8. 删除(Delete)
// =============================================
// 软删除(模型含 DeletedAt 时自动启用)
// =============================================
// 如果 User 嵌入了 gorm.Model(含 DeletedAt 字段),Delete 不会真正删除行
// 而是将 deleted_at 设为当前时间
db.Delete(&user)
// 生成 SQL: UPDATE users SET deleted_at = '2024-03-15 ...' WHERE id = 1
// 记录还在数据库中,只是被标记为"已删除"
db.Delete(&user, 1)
// 生成 SQL: UPDATE users SET deleted_at = '...' WHERE id = 1
// 通过主键指定要删除的记录
db.Delete(&User{}, []int{1, 2, 3})
// 生成 SQL: UPDATE users SET deleted_at = '...' WHERE id IN (1, 2, 3)
// 批量软删除
// 条件删除
db.Where("age < ?", 10).Delete(&User{})
// 生成 SQL: UPDATE users SET deleted_at = '...' WHERE age < 10 AND deleted_at IS NULL
// ===================== 软删除的查询行为 =====================
// 默认查询自动排除已软删除的记录
db.Find(&users)
// 生成 SQL: SELECT * FROM users WHERE deleted_at IS NULL
// 不会查到已删除的记录
db.Where("name = ?", "Alice").First(&user)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice' AND deleted_at IS NULL ORDER BY id LIMIT 1
// 查找包含已软删除记录
db.Unscoped().Where("name = ?", "Alice").Find(&users)
// 生成 SQL: SELECT * FROM users WHERE name = 'Alice'
// Unscoped() 去掉 deleted_at IS NULL 条件,能查到已删除的
// ===================== 永久删除(物理删除)=====================
db.Unscoped().Delete(&user)
// 生成 SQL: DELETE FROM users WHERE id = 1
// 真正从数据库删除这一行,不可恢复
// ===================== 防止误删全表 =====================
// ⚠️ 不带条件的删除会被 GORM 阻止,返回 ErrMissingWhereClause
// db.Delete(&User{}) → 报错!
// 如果确实需要删除全部记录:
db.Where("1 = 1").Delete(&User{})
// 生成 SQL: UPDATE users SET deleted_at = '...' WHERE 1 = 1 AND deleted_at IS NULL
// 或者在配置中全局允许(不推荐):
// gorm.Config{AllowGlobalUpdate: true}
9. 原生 SQL
// ===================== Raw 查询 =====================
// 执行原生 SELECT,结果映射到结构体
var users []User
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
// 直接执行: SELECT * FROM users WHERE age > 18
// 结果扫描到 []User
// 映射到自定义结构体
var result struct {
Total int64
Avg float64
}
db.Raw("SELECT COUNT(*) as total, AVG(age) as avg FROM users").Scan(&result)
// result.Total = 用户总数, result.Avg = 平均年龄
// ===================== Exec 执行(非查询语句)=====================
db.Exec("UPDATE users SET name = ? WHERE id = ?", "new_name", 1)
// 执行原生 UPDATE
db.Exec("DROP TABLE IF EXISTS temp_users")
// 执行原生 DDL
// ===================== SQL Builder =====================
// 不写完整 SQL,用 GORM 的链式方法拼接
var results []map[string]interface{}
db.Table("users").
Select("name", "age").
Where("id > ?", 5).
Scan(&results)
// 生成 SQL: SELECT name, age FROM users WHERE id > 5
// Table("users") → 指定表名(不依赖 Model)
// ===================== Row / Rows =====================
// 查询单行
row := db.Table("users").Where("name = ?", "Alice").Select("name", "age").Row()
// 生成 SQL: SELECT name, age FROM users WHERE name = 'Alice'
var name string
var age int
row.Scan(&name, &age) // 手动扫描每个列到变量
// 查询多行
rows, _ := db.Model(&User{}).Where("age > ?", 18).Rows()
// 生成 SQL: SELECT * FROM users WHERE age > 18
defer rows.Close() // ⚠️ 必须关闭,否则连接泄漏
for rows.Next() {
var user User
db.ScanRows(rows, &user) // 将当前行扫描到 User 结构体(比手动 Scan 方便)
fmt.Println(user)
}
// ===================== DryRun 模式 =====================
// 只生成 SQL,不真正执行,用于调试
stmt := db.Session(&gorm.Session{DryRun: true}).
Where("name = ?", "Alice").
Find(&User{}).Statement
fmt.Println(stmt.SQL.String()) // "SELECT * FROM users WHERE name = ? AND deleted_at IS NULL"
fmt.Println(stmt.Vars) // ["Alice"]
// 可以看到生成的 SQL 和绑定参数,不会实际查数据库
10. 关联关系
// =============================================
// Belongs To(属于)一对一/一对多
// =============================================
// 一个 Order 属于一个 User
// 外键放在 Order 表中("子"表持有外键)
type Order struct {
gorm.Model
Amount float64
UserID uint // 外键,默认命名规则:关联结构体名 + "ID" → UserID
User User // 关联字段:通过 UserID 找到对应的 User
// 建表后 orders 表有 user_id 列,指向 users 表的 id
}
// 自定义外键名
type Order2 struct {
gorm.Model
Amount float64
CreatorID uint // 外键字段叫 CreatorID 而非 UserID
Creator User `gorm:"foreignKey:CreatorID"` // 告诉 GORM:用 CreatorID 关联 User
// 建表后 orders 表有 creator_id 列
}
// =============================================
// Has One(拥有一个)1对1
// =============================================
// 一个 User 拥有一个 Profile
// 外键在 Profile 表中("子"表持有外键)
type User struct {
gorm.Model
Name string
Profile Profile // has one:User 拥有一个 Profile
}
type Profile struct {
gorm.Model
UserID uint // 外键:指向 users 表的 id
Bio string
Avatar string
// 建表后 profiles 表有 user_id 列
}
// Has One vs Belongs To 的区别:
// Has One: 外键在"对方"表(Profile 表有 user_id)
// Belongs To: 外键在"自己"表(Order 表有 user_id)
// 本质上是从哪个方向看的区别
// =============================================
// Has Many(拥有多个)一对多
// =============================================
// 一个 User 拥有多个 Order
type User struct {
gorm.Model
Name string
Orders []Order // has many:用切片表示"多个"
}
// 外键在 Order 表中(Order.UserID → User.ID)
// GORM 自动根据 User → Order 的关系找到 Order.UserID 作为外键
// =============================================
// Many To Many(多对多)
// =============================================
// 一个 User 可以学多门语言,一门语言可以被多个 User 学习
type User struct {
gorm.Model
Name string
Languages []Language `gorm:"many2many:user_languages;"`
// many2many:user_languages → 指定中间表名为 user_languages
// GORM 会自动创建中间表 user_languages,包含 user_id 和 language_id 两列
}
type Language struct {
gorm.Model
Name string
}
// 中间表结构(GORM 自动创建):
// user_languages:
// user_id → 外键指向 users.id
// language_id → 外键指向 languages.id
// 自定义中间表(需要额外字段时)
type UserLanguage struct {
UserID uint `gorm:"primaryKey"`
LanguageID uint `gorm:"primaryKey"`
Skill int // 额外字段:熟练度
}
// 需要用 SetupJoinTable 注册自定义中间表
db.SetupJoinTable(&User{}, "Languages", &UserLanguage{})
// =============================================
// 多态关联
// =============================================
// Comment 可以属于 Article 或 Video(不同类型的"父"表)
type Comment struct {
gorm.Model
Content string
CommentableID uint // 父记录的 ID(如 Article 的 ID 或 Video 的 ID)
CommentableType string // 父记录的类型("articles" 或 "videos")
}
type Article struct {
gorm.Model
Title string
Comments []Comment `gorm:"polymorphic:Commentable;"`
// polymorphic:Commentable → 使用 CommentableID + CommentableType 作为多态外键
}
type Video struct {
gorm.Model
URL string
Comments []Comment `gorm:"polymorphic:Commentable;"`
}
// 当为 Article(ID=1)创建 Comment 时:
// CommentableID = 1, CommentableType = "articles"
// 当为 Video(ID=5)创建 Comment 时:
// CommentableID = 5, CommentableType = "videos"
// =============================================
// Association Mode(关联操作)
// =============================================
// 创建时自动写关联
db.Create(&User{
Name: "Alice",
Orders: []Order{{Amount: 100}, {Amount: 200}},
})
// 生成 SQL:
// INSERT INTO users (name, ...) VALUES ('Alice', ...)
// INSERT INTO orders (user_id, amount, ...) VALUES (1, 100, ...), (1, 200, ...)
// 自动将 user_id 填入 orders 表
// 添加关联(给已有 user 追加一个 order)
db.Model(&user).Association("Orders").Append(&Order{Amount: 300})
// 生成 SQL: INSERT INTO orders (user_id, amount, ...) VALUES (1, 300, ...)
// 替换关联(清空原有关联,用新的替换)
db.Model(&user).Association("Languages").Replace(&Language{Name: "Go"}, &Language{Name: "Rust"})
// 先清除 user_languages 中该 user 的所有记录,再插入新的关联
// 删除关联(解除关系,不删除记录本身)
db.Model(&user).Association("Orders").Delete(&order)
// Has Many/Many2Many: 清除外键(user_id 设为 NULL 或删除中间表记录)
// ⚠️ 不会删除 orders 表中的记录本身
// 清空关联(解除该 user 的所有 orders 关联)
db.Model(&user).Association("Orders").Clear()
// 将所有关联 order 的 user_id 设为 NULL(或删除中间表记录)
// 关联计数
count := db.Model(&user).Association("Orders").Count()
// 生成 SQL: SELECT COUNT(*) FROM orders WHERE user_id = 1
// count = 该 user 有多少个 order
// 查找关联
var orders []Order
db.Model(&user).Association("Orders").Find(&orders)
// 生成 SQL: SELECT * FROM orders WHERE user_id = 1
// orders = 该 user 的所有订单
// 带条件查找关联
db.Model(&user).Association("Orders").Find(&orders, "amount > ?", 50)
// 生成 SQL: SELECT * FROM orders WHERE user_id = 1 AND amount > 50
11. 预加载
var users []User
// =============================================
// Preload(每个关联单独一条 SQL)
// =============================================
// --- 基本预加载 ---
db.Preload("Orders").Find(&users)
// 生成 2 条 SQL:
// ① SELECT * FROM users
// ② SELECT * FROM orders WHERE user_id IN (1, 2, 3, ...)
// 查完 users 后,自动把每个 user 的 orders 填充到 user.Orders 字段
// 如果 users 有 100 个,只发 2 条 SQL(不是 101 条,解决了 N+1 问题)
// --- 条件预加载 ---
db.Preload("Orders", "amount > ?", 100).Find(&users)
// 生成 2 条 SQL:
// ① SELECT * FROM users
// ② SELECT * FROM orders WHERE user_id IN (...) AND amount > 100
// 只预加载 amount > 100 的订单
// --- 自定义预加载 ---
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 50).Order("created_at DESC").Limit(5)
}).Find(&users)
// 生成 2 条 SQL:
// ① SELECT * FROM users
// ② SELECT * FROM orders WHERE user_id IN (...) AND amount > 50 ORDER BY created_at DESC LIMIT 5
// ⚠️ Limit(5) 是全局 Limit,不是每个 user 取 5 条!
// 如果想每个 user 取最新 5 个 order,需要用子查询或应用层处理
// --- 嵌套预加载 ---
db.Preload("Orders.Items").Find(&users)
// 生成 3 条 SQL:
// ① SELECT * FROM users
// ② SELECT * FROM orders WHERE user_id IN (...)
// ③ SELECT * FROM items WHERE order_id IN (...)
// 先加载 users → 再加载每个 user 的 orders → 再加载每个 order 的 items
// 同时预加载多个关联
db.Preload("Orders").Preload("Profile").Find(&users)
// 生成 3 条 SQL:
// ① SELECT * FROM users
// ② SELECT * FROM orders WHERE user_id IN (...)
// ③ SELECT * FROM profiles WHERE user_id IN (...)
// --- 预加载全部关联 ---
db.Preload(clause.Associations).Find(&users)
// 自动预加载 User 的所有直接关联(Orders、Profile 等)
// ⚠️ 只加载第一层,不会递归加载嵌套关联(如 Orders.Items)
// =============================================
// Joins 预加载(单条 SQL,适合一对一 / Belongs To)
// =============================================
db.Joins("Profile").Find(&users)
// 生成 1 条 SQL:
// SELECT users.*, profiles.bio, profiles.avatar, ...
// FROM users LEFT JOIN profiles ON profiles.user_id = users.id
// 用 LEFT JOIN 一次性查出 users 和 profiles
// 比 Preload 少一条 SQL,性能更好
// 条件 Joins 预加载
db.Joins("Profile", db.Where(&Profile{Bio: "engineer"})).Find(&users)
// 生成 SQL:
// SELECT users.*, profiles.*
// FROM users LEFT JOIN profiles ON profiles.user_id = users.id AND profiles.bio = 'engineer'
// 只 JOIN bio = 'engineer' 的 profile
// =============================================
// Preload vs Joins 选择
// =============================================
//
// Preload:
// - 发多条 SQL(1 + N 个关联 = 1+N 条,不是 1+每行)
// - 适合 Has Many / Many To Many(一对多、多对多)
// - 不改变主查询结果的行数
//
// Joins:
// - 发 1 条 SQL(LEFT JOIN)
// - 适合 Has One / Belongs To(一对一)
// - ⚠️ 用于 Has Many 时会导致主记录重复(JOIN 展开)
12. 事务
// =============================================
// 自动事务(推荐)
// =============================================
// db.Transaction 自动管理 Begin / Commit / Rollback
err := db.Transaction(func(tx *gorm.DB) error {
// ⚠️ 在事务内部必须使用 tx 而非 db,否则操作不在事务中
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
return err // 返回任何 error → 自动 ROLLBACK,所有操作撤销
}
if err := tx.Create(&Order{Amount: 100, UserID: 1}).Error; err != nil {
return err // 这里出错 → 上面的 User 也会被回滚
}
return nil // 返回 nil → 自动 COMMIT,所有操作生效
})
// err != nil 说明事务失败(已回滚)
// =============================================
// 嵌套事务 / SavePoint
// =============================================
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1) // 操作 1
// 嵌套事务:内部会自动创建 SAVEPOINT
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2) // 操作 2
return errors.New("rollback inner")
// 返回 error → ROLLBACK TO SAVEPOINT → 只有 user2 被回滚
})
// user1 不受影响,外层事务继续
return nil // user1 正常提交
})
// 手动 SavePoint
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1) // 操作 1
tx.SavePoint("sp1") // 创建保存点 sp1
tx.Create(&user2) // 操作 2
tx.RollbackTo("sp1") // 回滚到 sp1 → user2 撤销
return nil // user1 正常提交,user2 已被回滚
})
// =============================================
// 手动事务
// =============================================
tx := db.Begin() // 开始事务
if err := tx.Error; err != nil {
return err
}
// ⚠️ 推荐用 defer 兜底,防止忘记 Rollback 导致连接泄漏
defer func() {
if r := recover(); r != nil {
tx.Rollback() // panic 时回滚
}
}()
if err := tx.Create(&user).Error; err != nil {
tx.Rollback() // 出错回滚
return err
}
if err := tx.Create(&order).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit() // 全部成功,提交
// ⚠️ 手动事务的风险:
// 忘记 Rollback → 数据库连接一直被占用(连接泄漏)
// 忘记 Commit → 事务超时,所有操作都不生效
// 所以强烈推荐使用 db.Transaction() 自动管理
13. Hook(钩子)
// Hook 是在数据库操作前后自动调用的方法
// 执行顺序:
// 创建: BeforeSave → BeforeCreate → [INSERT] → AfterCreate → AfterSave
// 更新: BeforeSave → BeforeUpdate → [UPDATE] → AfterUpdate → AfterSave
// 删除: BeforeDelete → [DELETE] → AfterDelete
// 查询: [SELECT] → AfterFind
//
// 如果任何 Before* Hook 返回 error → 中断操作,不执行 SQL,事务中会自动回滚
type User struct {
gorm.Model
Name string
UUID string
Age int
}
// 创建前自动生成 UUID
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.UUID = uuid.New().String()
// 在 INSERT 执行之前,u.UUID 已被赋值
// 所以插入的记录会带有 UUID
return nil
}
// 创建 + 更新前都会触发的校验
func (u *User) BeforeSave(tx *gorm.DB) error {
if u.Age < 0 {
return errors.New("age cannot be negative")
// 返回 error → SQL 不会执行 → 事务回滚
}
return nil
}
// 查询后处理(每条记录查出来后都会调用)
func (u *User) AfterFind(tx *gorm.DB) error {
// 比如:解密手机号、拼接头像 URL 等
// u.Phone = decrypt(u.Phone)
return nil
}
// 删除前检查
func (u *User) BeforeDelete(tx *gorm.DB) error {
var count int64
tx.Model(&Order{}).Where("user_id = ?", u.ID).Count(&count)
if count > 0 {
return errors.New("cannot delete user with orders")
// 有订单的用户不允许删除
}
return nil
}
// ===================== 跳过 Hook =====================
// 某些批量操作不想触发 Hook(比如数据迁移),用 SkipHooks
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
// 不触发 BeforeSave / BeforeCreate / AfterCreate / AfterSave
14. Scopes 与链式调用
// Scope 是可复用的查询片段,类型签名为 func(*gorm.DB) *gorm.DB
// 把常用的查询逻辑封装成 Scope,在不同地方复用
// ===================== 通用分页 Scope =====================
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
return db.Offset((page - 1) * pageSize).Limit(pageSize)
}
}
// 使用: db.Scopes(Paginate(2, 20)).Find(&users)
// 生成 SQL: SELECT * FROM users LIMIT 20 OFFSET 20 (第 2 页,每页 20 条)
// ===================== 状态过滤 Scope =====================
func Active(db *gorm.DB) *gorm.DB {
return db.Where("active = ?", true)
}
// 使用: db.Scopes(Active).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE active = true
// ===================== 带参数的 Scope =====================
func CreatedAfter(t time.Time) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("created_at > ?", t)
}
}
func WithRole(role string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("role = ?", role)
}
}
// ===================== 组合使用多个 Scope =====================
db.Scopes(Active, WithRole("admin"), Paginate(1, 20)).Find(&users)
// 生成 SQL: SELECT * FROM users WHERE active = true AND role = 'admin' LIMIT 20 OFFSET 0
// 多个 Scope 的条件用 AND 连接
db.Scopes(
CreatedAfter(time.Now().AddDate(0, -1, 0)), // 最近一个月
Paginate(2, 50), // 第 2 页,每页 50 条
).Find(&orders)
// 生成 SQL: SELECT * FROM orders WHERE created_at > '2024-02-15 ...' LIMIT 50 OFFSET 50
// ===================== 条件性 Scope =====================
// 有值才加条件,没值就跳过(适合动态搜索场景)
func FilterByName(name string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if name != "" {
return db.Where("name LIKE ?", "%"+name+"%")
}
return db // name 为空时不加任何条件
}
}
func FilterByAgeRange(minAge, maxAge int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if minAge > 0 {
db = db.Where("age >= ?", minAge)
}
if maxAge > 0 {
db = db.Where("age <= ?", maxAge)
}
return db
}
}
// 动态搜索:用户可能只填了部分条件
db.Scopes(
FilterByName(nameInput), // nameInput = "" 时不加条件
FilterByAgeRange(minAgeInput, 0), // maxAge = 0 时不限制上限
Paginate(pageInput, sizeInput),
).Find(&users)
15. 泛型 API(v1.30+)
// GORM v1.30 引入泛型 API,核心改进:
// ✅ 返回值直接是目标类型(不需要预声明变量再传指针)
// ✅ 强制要求 context.Context(更规范的超时 / 取消控制)
// ✅ 编译器类型检查(传错类型编译报错)
import (
"context"
"gorm.io/gorm"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
ctx := context.Background()
// ===================== Create =====================
err := gorm.G[Product](db).Create(ctx, &Product{Code: "D42", Price: 100})
// 等价传统写法: db.WithContext(ctx).Create(&Product{Code: "D42", Price: 100})
// ===================== 查询单条 =====================
product, err := gorm.G[Product](db).Where("code = ?", "D42").First(ctx)
// 返回 (Product, error) ← 直接返回 Product 值,不需要预声明 var product Product
// 传统写法需要:
// var product Product
// err := db.WithContext(ctx).Where("code = ?", "D42").First(&product).Error
// ===================== 查询多条 =====================
products, err := gorm.G[Product](db).Where("price > ?", 50).Find(ctx)
// 返回 ([]Product, error) ← 直接返回切片
// 传统写法:
// var products []Product
// db.WithContext(ctx).Where("price > ?", 50).Find(&products)
// ===================== 更新 =====================
err = gorm.G[Product](db).Where("id = ?", product.ID).Update(ctx, "Price", 200)
// 生成 SQL: UPDATE products SET price = 200, updated_at = '...' WHERE id = 1
// 多字段更新
err = gorm.G[Product](db).Where("id = ?", product.ID).Updates(ctx, Product{Code: "E42", Price: 300})
// 生成 SQL: UPDATE products SET code = 'E42', price = 300, updated_at = '...' WHERE id = 1
// ⚠️ 和传统 API 一样,struct 零值字段会被忽略
// ===================== 删除 =====================
err = gorm.G[Product](db).Where("id = ?", product.ID).Delete(ctx)
// 生成 SQL: UPDATE products SET deleted_at = '...' WHERE id = 1(软删除)
}
16. Session 与上下文
// =============================================
// Session:创建新的 DB 会话,避免条件污染
// =============================================
// ⚠️ 问题场景:
// tx := db.Where("name = ?", "Alice")
// tx.Find(&users1) // ← 带 name='Alice' 条件
// tx.Find(&users2) // ← 也带 name='Alice' 条件!条件被"污染"了
// 解决:用 Session 创建干净的会话
// NewDB 模式:不继承之前的 Where 条件
tx := db.Where("name = ?", "Alice").Session(&gorm.Session{NewDB: true})
tx.Find(&users)
// 生成 SQL: SELECT * FROM users
// ← 没有 name='Alice' 条件,因为 NewDB: true 抛弃了之前的条件
// DryRun:只生成 SQL 不执行(调试用)
stmt := db.Session(&gorm.Session{DryRun: true}).Create(&user).Statement
fmt.Println(stmt.SQL.String()) // 打印 INSERT 语句
fmt.Println(stmt.Vars) // 打印绑定参数
// 数据库没有任何操作
// PrepareStmt:开启预编译语句缓存
// 同一条 SQL 模板只编译一次,后续只传参数,提升高频查询性能
tx = db.Session(&gorm.Session{PrepareStmt: true})
// SkipHooks:跳过所有 Hook
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
// BeforeCreate、AfterCreate 等都不触发
// =============================================
// Context
// =============================================
// 传入 context.Context 实现超时控制、取消信号
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 5 秒超时:如果查询 5 秒还没完成,自动取消
db.WithContext(ctx).Find(&users)
// 生成 SQL 正常,但如果执行超过 5 秒,会被 context 取消,返回 context.DeadlineExceeded
db.WithContext(ctx).Create(&user)
// 同理,INSERT 超时也会被取消
17. 自定义数据类型
import (
"database/sql/driver"
"encoding/json"
"strings"
)
// =============================================
// 自定义类型:实现 Scanner + Valuer 接口
// =============================================
// Scanner: 从数据库读取时调用([]byte → Go 类型)
// Valuer: 写入数据库时调用(Go 类型 → 数据库可接受的值)
// --- JSON 类型 ---
// 将 map[string]interface{} 存为数据库的 JSON 列
type JSON map[string]interface{}
// 从数据库读取时调用:数据库的 JSON 字符串 → Go 的 map
func (j *JSON) Scan(value interface{}) error {
bytes, ok := value.([]byte) // 数据库驱动返回 []byte
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(bytes, j) // JSON 字符串 → map
}
// 写入数据库时调用:Go 的 map → JSON 字符串
func (j JSON) Value() (driver.Value, error) {
return json.Marshal(j) // map → JSON 字符串 → 存入数据库
}
// 使用
type Product struct {
gorm.Model
Name string
Attrs JSON `gorm:"type:json"` // MySQL JSON 列类型
}
db.Create(&Product{
Name: "Phone",
Attrs: JSON{"color": "black", "storage": "128GB"},
})
// 存入数据库的 attrs 列内容: {"color":"black","storage":"128GB"}
var p Product
db.First(&p, 1)
fmt.Println(p.Attrs["color"]) // "black" ← 自动从 JSON 字符串反序列化为 map
// --- StringSlice 类型 ---
// 将 []string 存为逗号分隔的字符串(如 "go,rust,python")
type StringSlice []string
func (s *StringSlice) Scan(value interface{}) error {
str, ok := value.(string) // 数据库返回 string
if !ok {
bytes, ok := value.([]byte)
if !ok {
return errors.New("unsupported type")
}
str = string(bytes)
}
*s = strings.Split(str, ",") // "go,rust,python" → ["go", "rust", "python"]
return nil
}
func (s StringSlice) Value() (driver.Value, error) {
return strings.Join(s, ","), nil // ["go", "rust", "python"] → "go,rust,python"
}
// --- GORM 内置 Serializer(更简便的方式)---
// 不需要手写 Scanner/Valuer,加 serializer tag 即可
type UserPreference struct {
gorm.Model
Settings map[string]interface{} `gorm:"serializer:json"` // 自动用 JSON 序列化/反序列化
Tags []string `gorm:"serializer:json"` // 也可以是切片
}
// GORM 会自动在读写时调用 json.Marshal / json.Unmarshal
18. 性能优化
// ===================== 1. 跳过默认事务 =====================
// GORM 默认每个 Create/Update/Delete 都包在一个事务里
// 如果你的场景不需要事务(如只读、或自己手动管理事务),可以关闭,约提升 30%
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true,
})
// ===================== 2. 预编译缓存 =====================
// 同一条 SQL 模板只编译一次,后续只传参数
// 高频查询场景提升明显
db, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{
PrepareStmt: true,
})
// ===================== 3. 只查需要的列 =====================
db.Select("id", "name").Find(&users)
// 生成: SELECT id, name FROM users
// 避免 SELECT *,减少数据传输量,尤其是表有 TEXT/BLOB 大字段时
// ===================== 4. 批量插入 =====================
db.CreateInBatches(&users, 100) // 每批 100 条
// 比逐条 Insert 快数十倍
// ===================== 5. 分批处理大量数据 =====================
db.Where("active = ?", true).FindInBatches(&users, 500, func(tx *gorm.DB, batch int) error {
for _, user := range users {
// 在一批中一个一个处理 user
}
// 每次只加载 500 条到内存,处理完再加载下一批
// 百万级数据也不会 OOM
return nil
})
// ===================== 6. 用 Map 接收结果 =====================
// 避免反射开销(GORM 将 row → struct 需要用反射)
var results []map[string]interface{}
db.Model(&User{}).Find(&results)
// 结果直接存到 map,省去了反射映射的开销
// ===================== 7. 索引优化 =====================
type User struct {
gorm.Model
Email string `gorm:"uniqueIndex"` // 唯一索引:加速 WHERE email = ?
Name string `gorm:"index"` // 普通索引:加速 WHERE name = ?
Age int `gorm:"index:idx_age_active"` // 复合索引
Active bool `gorm:"index:idx_age_active"` // (age, active) 组合查询更快
}
// ===================== 8. Joins 替代 Preload =====================
// 一对一场景用 Joins 比 Preload 少一条 SQL
db.Joins("Profile").Find(&users) // 1 条 SQL(LEFT JOIN)
db.Preload("Profile").Find(&users) // 2 条 SQL
// ===================== 9. 连接池调优 =====================
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10) // 空闲连接保持 10 个
sqlDB.SetMaxOpenConns(100) // 最多同时 100 个连接
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最多存活 1 小时
// 根据实际并发量和数据库配置调整
19. 常用配置
import (
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 跳过默认事务(提升写入性能)
SkipDefaultTransaction: true,
// 命名策略
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // 所有表名加前缀:User → t_users
SingularTable: true, // 使用单数表名:User → t_user(而非 t_users)
NoLowerCase: false, // false = 转小写(默认)
},
// 日志级别
// Silent → 不输出任何日志
// Error → 只输出 error
// Warn → error + 慢 SQL
// Info → 全部 SQL(开发环境推荐)
Logger: logger.Default.LogMode(logger.Info),
// 预编译缓存
PrepareStmt: true,
// 禁止自动创建外键约束
// AutoMigrate 时不会在数据库创建 FOREIGN KEY 约束
// 很多团队不用数据库外键,只在应用层维护关系
DisableForeignKeyConstraintWhenMigrating: true,
// 允许全局 Update / Delete(默认不允许无条件更新/删除,防止误操作)
// AllowGlobalUpdate: true, // ⚠️ 谨慎开启
})
// ===================== 自定义 Logger =====================
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // 输出目标
logger.Config{
SlowThreshold: 200 * time.Millisecond, // 慢 SQL 阈值(超过 200ms 输出 WARN)
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true, // 忽略 RecordNotFound 错误的日志(太多了没意义)
Colorful: true, // 彩色输出(终端好看点)
},
)
db, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: newLogger})
// ===================== Database Resolver(读写分离)=====================
import "gorm.io/plugin/dbresolver"
db.Use(dbresolver.Register(dbresolver.Config{
Sources: []gorm.Dialector{mysql.Open(dsnWrite)}, // 写库(主库)
Replicas: []gorm.Dialector{mysql.Open(dsnRead1), mysql.Open(dsnRead2)}, // 读库(从库,可多个)
Policy: dbresolver.RandomPolicy{}, // 负载均衡策略:随机
}).SetMaxIdleConns(10).SetMaxOpenConns(100))
// 效果:
// db.Create / db.Update / db.Delete → 自动路由到 Sources(写库)
// db.Find / db.First → 自动路由到 Replicas(读库,随机选一个)
20. 常见易错点
// =============================================
// ❌ 1. struct 更新零值被忽略(最常见的坑)
// =============================================
db.Model(&user).Updates(User{Name: "Alice", Age: 0})
// 生成 SQL: UPDATE users SET name = 'Alice', updated_at = '...' WHERE id = 1
// ❌ Age = 0 被忽略了!因为 0 是 int 的零值,GORM 认为你没赋值
// ✅ 解法一:用 Map(零值不会被忽略)
db.Model(&user).Updates(map[string]interface{}{"name": "Alice", "age": 0})
// 生成 SQL: UPDATE users SET name = 'Alice', age = 0, updated_at = '...' WHERE id = 1
// ✅ 解法二:用 Select 强制指定要更新的字段
db.Model(&user).Select("Name", "Age").Updates(User{Name: "Alice", Age: 0})
// 生成 SQL: UPDATE users SET name = 'Alice', age = 0, updated_at = '...' WHERE id = 1
// 同理,Where struct 也有这个问题:
db.Where(&User{Active: false}).Find(&users)
// ❌ Active = false 是零值,被忽略 → 变成 SELECT * FROM users(无条件)
// ✅ 用 Map: db.Where(map[string]interface{}{"active": false}).Find(&users)
// =============================================
// ❌ 2. 链式调用条件污染
// =============================================
// 错误用法:在全局 db 上加 Where 条件,会影响后续所有查询
tx := db.Where("name = ?", "Alice")
tx.Find(&users1) // 生成: WHERE name = 'Alice' ← 正常
tx.Find(&users2) // 生成: WHERE name = 'Alice' ← 也带上了!
// ✅ 解法:每次查询用新的 Session
tx := db.Session(&gorm.Session{NewDB: true})
// 或者直接用全局 db,每次 db.Where(...).Find(...) 不会互相影响
// (GORM 的链式调用会创建新的 Statement,全局 db 本身是安全的)
// =============================================
// ❌ 3. First 找不到记录会返回 error
// =============================================
err := db.First(&user, 999).Error
// 如果 id=999 的记录不存在:
// err = gorm.ErrRecordNotFound(不是 nil)
// user 为零值
// ⚠️ 很多人没检查这个 error,以为查不到就是零值
// ✅ 正确处理:
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("用户不存在")
} else if err != nil {
fmt.Println("数据库错误:", err)
}
// 或者用 Find(找不到不报错,只是空切片):
var users []User
db.Where("id = ?", 999).Find(&users)
if len(users) == 0 {
fmt.Println("用户不存在")
}
// =============================================
// ❌ 4. 忘记处理 error
// =============================================
db.Create(&user) // 如果主键冲突、字段超长等原因失败了,你永远不知道
// ✅ 每次操作都检查 error
if err := db.Create(&user).Error; err != nil {
log.Fatal(err)
}
// =============================================
// ❌ 5. Save 更新全部字段(包括零值)
// =============================================
user.Age = 0 // 原本 Age = 25
db.Save(&user)
// 生成 SQL: UPDATE users SET name='Alice', age=0, email='alice@go.dev', ... WHERE id=1
// ⚠️ Save 把所有字段都写了一遍,Age 被覆盖为 0
// ✅ 如果只想更新特定字段,用 Updates
db.Model(&user).Updates(map[string]interface{}{"age": 0})
// =============================================
// ❌ 6. 无条件删除/更新被阻止
// =============================================
db.Delete(&User{})
// ❌ 报错: WHERE conditions required(ErrMissingWhereClause)
// GORM 防止你意外删除全表
db.Model(&User{}).Update("active", false)
// ❌ 同样报错,防止你意外更新全表
// ✅ 如果确实要操作全部记录:
db.Where("1 = 1").Delete(&User{})
db.Model(&User{}).Where("1 = 1").Update("active", false)
// =============================================
// ❌ 7. N+1 查询问题
// =============================================
db.Find(&users) // 查出 100 个用户
for _, user := range users {
db.Model(&user).Association("Orders").Find(&orders)
// ❌ 每个用户查一次 orders → 100 + 1 = 101 条 SQL
}
// ✅ 用 Preload 一次性加载(只需 2 条 SQL)
db.Preload("Orders").Find(&users)
// =============================================
// ❌ 8. 软删除后忘记 Unscoped
// =============================================
db.Delete(&user, 1) // 软删除
db.Find(&users)
// 生成: SELECT * FROM users WHERE deleted_at IS NULL
// ❌ 已删除的 user 查不到
// ✅ 需要查已删除的记录时:
db.Unscoped().Find(&users)
// 生成: SELECT * FROM users(不加 deleted_at IS NULL)
// =============================================
// ❌ 9. 指望 AutoMigrate 删列
// =============================================
// 如果你从 User struct 里删掉了 Age 字段,然后运行 AutoMigrate
// ❌ 数据库的 age 列不会被删除!
// AutoMigrate 只增不减:加列加索引 ✅,删列改类型 ❌
// ✅ 生产环境用专用迁移工具: golang-migrate / goose / atlas
// =============================================
// ❌ 10. 并发场景共享 tx
// =============================================
tx := db.Where("name = ?", "Alice")
// ❌ 多个 goroutine 共享同一个 tx
go func() { tx.Find(&users1) }() // 可能出问题
go func() { tx.Find(&users2) }()
// ✅ 全局 db 是并发安全的,每个 goroutine 直接用 db:
go func() { db.Where("name = ?", "Alice").Find(&users1) }()
go func() { db.Where("name = ?", "Bob").Find(&users2) }()
// 每次 db.Where() 会创建新的 Statement,互不影响
本文覆盖 GORM 日常开发核心场景,相较于官方文档,更多代码示例说明,可收藏作为速查手册。官方文档参考 gorm.io/docs。

浙公网安备 33010602011771号