前语
go项目中经常需求查询db,依照以前java开发经验,会依据查询条件写许多办法,如:
- GetUserByUserID
- GetUsersByName
- GetUsersByAge
每一种查询条件写一个办法,这种办法对外是挺好的,对外遵循严厉原则,让每个对外的办法接口是明确的。可是对内的话,应该尽可能的通用,做到代码复用,少写代码,让代码看起来更优雅、整洁。
问题
在review代码的时分,针对上面3个办法,一般写法是
func GetUserByUserID(ctx context.Context, userID int64) (*User, error){
db := GetDB(ctx)
var user User
if userID > 0 {
db = db.Where(`userID = ?`, userID)
}
if err := db.Model(&User{}).Find(&user).Err; err != nil {
return nil, err
}
return user, nil
}
func GetUsersByName(ctx context.Context, name string) (*User, error){
db := GetDB(ctx)
var users []User
if name != "" {
db = db.Where(`name like '%%'`, name)
}
if err := db.Model(&User{}).Find(&users).Err; err != nil {
return nil, err
}
return users, nil
}
func GetUsersByAge(ctx context.Context, age int64) (*User, error){
db := GetDB(ctx)
var user User
if age > 0 {
db = db.Where(`age = ?`, age)
}
if err := db.Model(&User{}).Find(&user).Err; err != nil {
return nil, err
}
return user, nil
}
当User表上字段有几十个的时分,上面类似的办法会越来越多,代码没有做到复用。当有Teacher表、Class表等其他表的时分,上面的查询办法又要翻倍。
调用方也会写的很死,参数固定。当要添加一个查询条件的时分,要么改原来的函数,添加一个参数,这样其他调用的当地也都要改;要么新写一个函数,这样函数越来越多,难以维护和阅读。
上面是青铜写法,针对这种状况,下面介绍几种白银、黄金、王者写法
白银
将入参界说成一个结构体
type UserParam struct {
ID int64
Name string
Age int64
}
将入参都放在UserParam结构体中
funcGetUserInfo(ctxcontext.Context,info*UserParam)([]*User, error){
db:=GetDB(ctx)
db=db.Model(&User{})
varinfos[]*User
ifinfo.ID>0{
db=db.Where("user_id=?",info.ID)
}
ifinfo.Name!=""{
db=db.Where("user_name=?",info.Name)
}
ifinfo.Age>0{
db=db.Where("age=?",info.Age)
}
if err := db.Find(&infos).Err; err != nil {
return nil, err
}
returninfos, nil
}
这个代码写到这儿,比较最开始的办法其实现已好了不少,至少 dao 层的办法从许多个入参变成了一个,调用方的代码也能够依据自己的需求构建参数,不需求许多空占位符。可是存在的问题也比较显着:依然有许多判空不说,还引进了一个剩余的结构体。假如咱们就到此结束的话,多少有点惋惜。
别的,假如咱们再扩展一下业务场景,咱们运用的不是等值查询,而是多值查询或者区间查询,比方查询 status in (a, b),那上面的代码又怎样扩展呢?是不是又要引进一个办法,办法繁琐暂且不说,办法名叫啥都会让咱们纠结很久;或许能够尝试把每个参数都从单值扩展成数组,然后赋值的当地从 = 改为 in()的办法,一切参数查询都运用 in 显然对功能不是那么友好。
黄金
更高级的优化办法,是运用高阶函数。
type Option func(*gorm.DB)
界说 Option 是一个函数,这个函数的入参类型是*gorm.DB,回来值为空。
然后针对每一个需求查询的字段,界说一个高阶函数
funcUserID(IDint64)Option{
returnfunc(db*gorm.DB){
db.Where("`id`=?",ID)
}
}
funcName(nameint64)Option{
returnfunc(db*gorm.DB){
db.Where("`name`like %?%",name)
}
}
funcAge(ageint64)Option{
returnfunc(db*gorm.DB){
db.Where("`age`=?",age)
}
}
回来值是Option类型。
这样上面3个办法就能够合并成一个办法了
func GetUsersByCondition(ctx context.Context, opts ...Option)([]*User, error) {
db := GetDB(ctx)
for i:=range opts {
opts[i](db)
}
var users []User
if err := db.Model(&User{}).Find(&users).Err; err != nil {
return nil, err
}
return users, nil
}
没有比照就没有损伤,经过和最开始的办法比较,能够看到办法的入参由多个不同类型的参数变成了一组相同类型的函数,因此在处理这些参数的时分,也无需一个一个的判空,而是直接运用一个 for 循环就搞定,比较之前现已简洁了许多。
还能够扩展其他查询条件,比方IN,大于等
funcUserIDs(IDsint64)Option{
returnfunc(db*gorm.DB){
db.Where("`id`in(?)",IDs)
}
}
funcAgeGT(ageint64)Option{
returnfunc(db*gorm.DB){
db.Where("`age`>?",age)
}
}
并且这个查询条件最终是转换成Where条件,跟具体的表无关,也就是说这些界说是能够被其他表复用的。
王者
优化到上述办法现已能够了,可是王者一般会持续优化。
上述办法GetUsersByCondition只能查User表,能不能更通用一些,查任意表呢?共享GetUsersByCondition办法,发现假如要做到查任意表,有2个阻止:
- 表明是在办法中写死的
- 回来值界说的是[]*User,不能通用
针对第一个问题,咱们能够界说一个Option来实现
func TableName(tableName string) Option {
return func(db *grom.DB) {
db.Table(tableName)
}
}
针对第二个问题,能够将回来参数作为入参,经过引用的办法传进来
func GetRecords(ctx context.Context, in any, opts ...Option) {
db := GetDB(ctx)
for i:=range opts {
opts[i](db)
}
return db.Find(in).Err
}
// 调用:依据user name 和age 查询users
var users []User
if err := GetRecords(ctx, &users, TableName("user"), Name("张三"), Age(18)); err != nil {
// TODO
}
总结
这儿经过对 grom 查询条件的抽象,大大简化了对 DB 组合查询的写法,提高了代码的简洁。
参阅
[1]怎么运用高阶函数编程提高代码的简洁性
[2]GO 编程形式:FUNCTIONAL OPTIONS