更进一步理解不定参数在GORM中的优雅应用
写在前面
最近一段时间的工作主要以业务逻辑编写为主,重度使用了一些Go的框架,比如Web框架Gin、ORM框架GORM、模板渲染框架Simplate等等。在高强度的情况下编写高复杂度的代码,难免会遇到各种坑,自然而然地也会积累很多宝贵的经验,这些经验是我个人比较看重的,因此总乐意花一些时间总结。
上一篇博客写了在GROM中使用不定参数的技巧,有朋友私信询问,于是感觉有必要基于上一篇内容再续一篇把这个点讲透彻。
适应人群
入门√——初级——中级——高级;本文适应入门及以上。
GORM最佳实践之不定参数的用法(二)
再看GROM中不定参数的最佳实践
承接上一篇博客,看下面的代码:
// Topic 话题
type Topic struct {
gorm.Model
Title string `gorm:"index"`
Content string `gorm:"type:text"`
ViewCount int `json:"view_count"`
ReplyCount int `json:"reply_count"`
UserID int `gorm:"index" json:"user_id"`
}
// QueryByUserID 根据UserID获取话题
func (t *Topic) QueryByUserID(userID int, args ...interface{}) (topics []Topic, err error) {
db := config.DB.Where("user_id = ?", userID)
if len(args) >= 2 {
db = db.Where(args[0], args[1:]...)
} else if len(args) >= 1 {
db = db.Where(args[0])
}
err = db.Find(&topics).Error
return
}
为了根据用户ID(UserID)获取到相应的话题(Topic),我们定义了一个QueryByUserID
的方法,为了表意刻意传入了一个参数,同时在QueryByUserID
的最后强行添加了一个args
不定参数,从而接受调用时传入的附加的参数,在函数体内根据args
的长度对它进行不同方式的应用,达到了优雅扩展QueryByUserID
的功能。
在添加了不定参数的情况下,调用QueryByUserID
时如果想要进一步精细地搜索话题,比如想要获取UserID为1
且最近一周发布的话题,可以这么写了:
topics, err := (&Topic{}).QueryByUserID(1, "created_at > ?", time.Now().Add(-7*24*time.Hour)
// ... 其他对topics 与 err 的处理
总之,非常灵活!
所述最佳实践中不定参数的约定
在QueryByUserID
中根据传入的不定参数的数量,采用不同的传入方式,其实这里遵循了GORM中Where条件语句的作用方式。
// ...
if len(args) >= 2 {
db = db.Where(args[0], args[1:]...)
} else if len(args) >= 1 {
db = db.Where(args[0])
}
// ...
首先我们先把这里的约定给出来:
- 当传入的
args
长度为1时,如果args[0]
不是一个map[string]interface{}
的值,这个值必须能够作用到主键上面。比如1,表示主键ID为1(ID=1
)的记录;比如[]int{1,2},表示主键ID为1和2(ID IN (1,2)
)的记录; - 否则,当传入的
args
长度为1时,args[0]
必须是一个map[string]interface{}
的值; - 当传入的
args
长度大于等于2时,args[0]
必须是一个string
类型的值,且包含了args[1:]
中同等个数的占位符。打个比方,如果args[0]
真实值为"created_at > ? AND deleted_at > ?"
,那么args[1:]
则应该包含两个值,一个对应第一个问号(即创建时间),一个对应第二个问号(即删除时间)。
GORM中相关函数源码解读
Where函数
当在GORM中调用Where
方法时,通过查看源码可以知道,这个函数最底层也会使用一个不定参数,将传入到Where
第一个后面的参数看成一个数组进行处理。具体地,第一个参数保存在map[string]interface{}
类型的query
字段,其他参数保存在args
字段。
func (s *search) Where(query interface{}, values ...interface{}) *search {
s.whereConditions = append(s.whereConditions, map[string]interface{}{"query": query, "args": values})
return s
}
buildCondition 函数
通过分析源码,可以找到GORM中的Where
、Or
、Not
定义的搜索语句最终都通过 buildCondition函数 渲染成为SQL语句。
func (scope *Scope) buildCondition(clause map[string]interface{}, include bool) (str string) {
//...
// 这里会把传入Where的第一个参数拿出来,进行类型断言
switch value := clause["query"].(type) {
case sql.NullInt64:
// 如果传入Where的第一个参数是SQL中的空类型,执行逻辑...
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
//如果传入Where的第一个参数是一个数字,执行逻辑...
case []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []string, []interface{}:
//如果传入Where的第一个参数是一个数字类型的数组,执行逻辑...
case string:
//如果传入Where的第一个参数是一个字符串,结合后面的
// for _, arg := range args {...} 会把参数塞进
// 占位符...
case map[string]interface{}:
// 恩,map[string]interface{} 类型的值比较特殊
// 也就是说,如果在Where中只传入一个值,且这个值是
// 个map,则必须要是 map[string]interface{}类型
// 的值,不能是 map[string]string 或其他类型的值
// 否则就匹配到 interface{} 去了...
case interface{}:
// 最后,如果以上类型都不是,则执行到了这里...
default:
// 如果都没有匹配,报错...
}
// ...
args := clause["args"].([]interface{})
for _, arg := range args {
// 这里会遍历传入Where的第一个参数后面的值
// 来替换掉占位符...
}
// ...
}
其他问题
通过上面的分析可以知道,在Where中传入的不定参数具有一定的约束,第二 、三…个参数不能随便写。
如果期望传入多个查询条件
在这种情况下,建议通过构造不定参数中第一个string
类型的参数达成目的,比如同时通过创建时间和删除时间查找数据,则构建"created_at > ? AND deleted_at > ?"
这种语句,后面跟着传入创建时间与删除时间即可。
如果期望传入一个函数回调
在我所遇到的应用场景中,还没有传入回调的场景,就个人经验来看,在没有强规范制约时(当前确实想不到一种合适的规范),传入回调会增加函数复杂度,造成后期代码维护困难。
不定参数用在可有可无的搜索条件
推荐不定参数只用在那些可有可无的搜索条件,如果一个函数定义时已经明确知道函数中需要包含哪些参数,推荐把这些参数显式写在函数定义中,利用Go静态语言的特定,减少编码逻辑上的错误。
小结
本文介绍了使用GORM时加入不定参数后的灵活性,以及这种灵活性背后的约束。通过对GORM中 Where函数和 buildCondition函数 源码的分析,更进一步阐释了这种不定参数的应用规范(及其缘由):
- 当传入的
args
长度为1时,如果args[0]
不是一个map[string]interface{}
的值,这个值必须能够作用到主键上面; - 否则,当传入的
args
长度为1时,args[0]
必须是一个map[string]interface{}
的值; - 当传入的
args
长度大于等于2时,args[0]
必须是一个string
类型的值,且包含了args[1:]
中同等个数的占位符。
参考
- Go的ORM框架GORM最佳实践之不定参数的用法 GORM中不定参数最佳实践的第一篇博文
- gorm-DB-Where函数 GORM中Where函数源码
- gorm-search-Where函数 GORM中search对象对应的Where函数源码
- gorm-buildCondition函数 GORM中buildCondition函数源码
- Getting Started with GORM · GORM Guide GORM的官方文档