第19章:Scopes 作用域 —— 封装可复用查询
本章目标
学完本章后,你将能够:
- 理解 Scope 的概念和签名
- 编写可复用的 Scope 函数(分页、过滤、排序)
- 组合多个 Scope 构建复杂查询
- 使用闭包实现参数化 Scope
- 根据条件动态应用 Scope
- 建立项目级的 Scope 函数库
前置知识
- 需要先学习:第11-18章(查询能力)
- 需要了解:Go 的闭包和高阶函数
19.1 什么是 Scope
Scope 是一个接受 *gorm.DB 并返回 *gorm.DB 的函数:
go
// Scope 的签名
type Scope func(db *gorm.DB) *gorm.DB它让你把常用的查询片段封装起来,随处复用:
go
// 定义一个简单的 Scope
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "active")
}
// 使用
db.Scopes(ActiveUsers).Find(&users)
// SQL: SELECT * FROM `users` WHERE status='active'为什么需要 Scope
没有 Scope 的代码:
go
// 三个地方都要查活跃用户,每个地方都写一遍 Where
db.Where("status = ?", "active").Find(&users) // 第1处
db.Where("status = ?", "active").First(&user, id) // 第2处
db.Where("status = ?", "active").Count(&count) // 第3处有 Scope 的代码:
go
// 一次定义,到处复用
db.Scopes(ActiveUsers).Find(&users)
db.Scopes(ActiveUsers).First(&user, id)
db.Scopes(ActiveUsers).Count(&count)
// 如果过滤条件变了,只改 Scope 定义一处即可19.2 基础 Scope 示例
分页 Scope
go
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 > 100 {
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
// 使用
db.Scopes(Paginate(2, 10)).Find(&users)
// SQL: SELECT * FROM `users` LIMIT 10 OFFSET 10状态过滤 Scope
go
func WithStatus(status string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if status != "" {
return db.Where("status = ?", status)
}
return db // 空状态时不加条件
}
}
// 使用
db.Scopes(WithStatus("active")).Find(&users)时间范围 Scope
go
func CreatedBetween(start, end time.Time) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("created_at BETWEEN ? AND ?", start, end)
}
}
// 使用
weekAgo := time.Now().Add(-7 * 24 * time.Hour)
db.Scopes(CreatedBetween(weekAgo, time.Now())).Find(&users)19.3 Scope 的组合
多个 Scope 可以链式组合:
go
// 查询最近一周创建的活跃成年用户,分页展示
db.Scopes(
ActiveUsers, // status = 'active'
AdultUsers, // age >= 18
CreatedBetween(weekAgo, time.Now()), // 最近一周
Paginate(1, 20), // 分页
).Order("created_at desc").Find(&users)
// SQL:
// SELECT * FROM `users`
// WHERE status='active'
// AND age>=18
// AND created_at BETWEEN '2024-01-08' AND '2024-01-15'
// ORDER BY created_at DESC
// LIMIT 20 OFFSET 019.4 条件 Scope(按需生效)
通过闭包参数控制 Scope 是否生效:
go
// 只有当 keyword 非空时才添加 LIKE 条件
func WithKeyword(keyword string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if keyword != "" {
return db.Where("name LIKE ?", "%"+keyword+"%")
}
return db // 空关键词,不添加条件
}
}
// 只有当 minAge > 0 时才添加年龄条件
func WithMinAge(minAge int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if minAge > 0 {
return db.Where("age >= ?", minAge)
}
return db
}
}实际场景:复杂搜索 API
go
type UserSearchParams struct {
Keyword string
Status string
MinAge int
MaxAge int
City string
Page int
PageSize int
}
func SearchUsers(db *gorm.DB, params UserSearchParams) ([]User, int64, error) {
var users []User
var total int64
query := db.Model(&User{}).Scopes(
WithKeyword(params.Keyword),
WithStatus(params.Status),
WithMinAge(params.MinAge),
WithMaxAge(params.MaxAge),
WithCity(params.City),
)
// 先统计总数
query.Count(&total)
// 再查分页数据
err := query.Scopes(Paginate(params.Page, params.PageSize)).
Order("created_at desc").
Find(&users).Error
return users, total, err
}19.5 常用 Scope 模板库
go
// ============ 过滤类 ============
func WhereLike(column, keyword string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if keyword != "" {
return db.Where(column+" LIKE ?", "%"+keyword+"%")
}
return db
}
}
func WhereIn(column string, values []interface{}) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if len(values) > 0 {
return db.Where(column+" IN ?", values)
}
return db
}
}
func WhereDateBetween(column string, start, end *time.Time) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if start != nil {
db = db.Where(column+" >= ?", *start)
}
if end != nil {
db = db.Where(column+" <= ?", *end)
}
return db
}
}
// ============ 排序类 ============
func OrderBy(field string, desc bool) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if desc {
return db.Order(field + " desc")
}
return db.Order(field + " asc")
}
}
// ============ 分页类 ============
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)
}
}
// ============ 预加载类 ============
func WithPreload(associations ...string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
for _, assoc := range associations {
db = db.Preload(assoc)
}
return db
}
}19.6 Scope 中的 DB 操作(小心!)
Scope 不仅能加查询条件,还能做更多事:
go
// Scope 中执行额外操作(不推荐,但有时有用)
func AuditLog(db *gorm.DB) *gorm.DB {
// 记录查询日志
log.Printf("执行查询: %s", db.Statement.SQL.String())
return db
}
// 更实用的:自动添加租户隔离
func TenantScope(tenantID uint) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("tenant_id = ?", tenantID)
}
}
// 所有查询自动带上租户 ID,防止数据泄漏常见错误
错误1:Scope 改变了不该改变的东西
go
// ❌ Scope 中使用了不安全的修改
func BadScope(db *gorm.DB) *gorm.DB {
db.Statement.Table = "hacked" // 改了表名!
return db
}
// ✅ Scope 应该只用 Where/Order/Limit/Preload 等方法
func GoodScope(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "active")
}错误2:忘记 return
go
// ❌ Scope 函数没有返回
func ActiveUsers(db *gorm.DB) *gorm.DB {
db.Where("status = ?", "active") // 没有 return!
}
// Scope 不生效
// ✅ 必须 return
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "active")
}错误3:Scope 过于复杂
go
// ❌ 一个 Scope 做了太多事
func EverythingScope(db *gorm.DB) *gorm.DB {
return db.Where("...").Order("...").Limit(10).Preload("...").Joins("...")
}
// ✅ 拆分成多个单一职责的 Scope
db.Scopes(StatusFilter, DateRange, Paginate).Find(&users)本章小结
- Scope 是
func(*gorm.DB) *gorm.DB类型的函数 - 用闭包实现参数化的 Scope
db.Scopes(s1, s2, s3)组合多个 Scope- Scope 可复用、可测试、可管理
- 条件 Scope:参数为空时不添加条件(
return db) - 最佳实践:建立项目级
scopes/包
练习题
- 定义一个
WithStatusScope,接受状态参数,当参数非空时添加 WHERE 条件。 - 定义
WithDateRangeScope,接受起始和结束时间,用于过滤created_at。 - 组合使用三个 Scope 实现:查询上周创建的、活跃的、VIP 用户,并分页展示。
- 将第15章的分页逻辑改写为 Scope 版本。
- 写一个
SearchUsers函数,接收多个可选搜索条件,用 Scope 动态构建查询。 - (思考题)Scope 和直接链式调用
db.Where().Where()有什么区别?什么情况下 Scope 更好?