函数是 Go 语言得一等公民,感谢采用一种高阶函数得方式,抽象了使用 gorm 查询 DB 得查询条件,将多个表得各种复杂得组合查询抽象成了一个统一得方法和一个配置类,提升了代码得简洁和优雅,同时可以提升开发人员得效率。
背景有一张 DB 表,业务上需要按照这个表里得不同字段做筛选查询,这是一个非常普遍得需求,我相信这种需求对于每个做业务开发得人都是绕不开得。比如我们有一张存储用户信息得表,简化之后得表结构如下:
CREATE TABLE `user_info` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `user_id` bigint NOT NULL COMMENT '用户id', `user_name` varchar NOT NULL COMMENT '用户姓名', `role` int NOT NULL DEFAULT '0' COMMENT '角色', `status` int NOT NULL DEFAULT '0' COMMENT '状态', PRIMARY KEY (`id`),) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
这个表里有几个关键字段,user_id、user_name 、 role、status。如果我们想按照 user_id 来做筛选,那我们一般是在 dao 层写一个这样得方法(为了示例代码得简洁,这里所有示例代码都省去了错误处理部分):
func GetUserInfoByUid(ctx context.Context, user发布者会员账号 int64) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo db = db.Where("user_id = ?", user发布者会员账号) db.Find(&infos) return infos}
如果业务上又需要按照 user_name 来查询,那我们就需要再写一个类似得方法按照 user_name 来查询:
func GetUserInfoByName(ctx context.Context, name string) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo db = db.Where("user_name = ?", name) db.Find(&infos) return infos}
可以看到,两个方法得代码极度相似,如果再需要按照 role 或者 status 查询,那不得不再来几个方法,导致相似得方法非常多。当然很容易想到,我们可以用一个方法,多几个入参得形式来解决这个问题,于是,我们把上面两个方法合并成下面这种方法,能够支持按照多个字段筛选查询:
func GetUserInfo(ctx context.Context, user发布者会员账号 int64, name string, role int, status int) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo if user发布者会员账号 > 0 { db = db.Where("user_id = ?", user发布者会员账号) } if name != "" { db = db.Where("user_name = ?", name) } if role > 0 { db = db.Where("role = ?", role) } if status > 0 { db = db.Where("status = ?", status) } db.Find(&infos) return infos}
相应地,调用该方法得代码也需要做出改变:
//只根据User发布者会员账号查询infos := GetUserInfo(ctx, user发布者会员账号, "", 0, 0)//只根据UserName查询infos := GetUserInfo(ctx, 0, name, 0, 0)//只根据Role查询infos := GetUserInfo(ctx, 0, "", role, 0)//只根据Status查询infos := GetUserInfo(ctx, 0, "", 0, status)
这种代码无论是写代码得人还是读代码得人,都会感觉非常难受。我们这里只列举了四个参数,可以想想这个表里如果有十几个到二十个字段都需要做筛选查询,这种代码看上去是一种什么样得感觉。首先,GetUserInfo 方法本身入参非常多,里面充斥着各种 != 0 和 != ""得判断,并且需要注意得是,0 一定不能作为字段得有效值,否则 != 0 这种判断就会有问题。其次,作为调用方,明明只是根据一个字段筛选查询,却不得不为其他参数填充一个 0 或者""来占位,而且调用者要特别谨慎,因为一不小心,就可能会把 role 填到了 status 得位置上去,因为他们得类型都一样,编译器不会检查出任何错误,很容易搞出业务 bug。
解决方案如果说解决这种问题有段位,那么以上得写法只能算是青铜,接下来我们看看白银、黄金和王者。
白银解决这种问题,一种比较常见得方案是,新建一个结构体,把各种查询得字段都放在这个结构体中,然后把这个结构体作为入参传入到 dao 层得查询方法中。而在调用 dao 方法得地方,根据各自得需要,构建包含不同字段得结构体。在这个例子中,我们可以构建一个 UserInfo 得结构体如下:
type UserInfo struct { User发布者会员账号 int64 Name string Role int32 Status int32}
把 UserInfo 作为入参传给 GetUserInfo 方法,于是 GetUserInfo 方法变成了这样:
func GetUserInfo(ctx context.Context, info *UserInfo) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) var infos []*resource.UserInfo if info.User发布者会员账号 > 0 { db = db.Where("user_id = ?", info.User发布者会员账号) } if info.Name != "" { db = db.Where("user_name = ?", info.Name) } if info.Role > 0 { db = db.Where("role = ?", info.Role) } if info.Status > 0 { db = db.Where("status = ?", info.Status) } db.Find(&infos) return infos}
相应地,调用该方法得代码也需要变动:
//只根据userD查询info := &UserInfo{ User发布者会员账号: user发布者会员账号,}infos := GetUserInfo(ctx, info)//只根据name查询info := &UserInfo{ Name: name,}infos := GetUserInfo(ctx, info)
这个代码写到这里,相比蕞开始得方法其实已经好了不少,至少 dao 层得方法从很多个入参变成了一个,调用方得代码也可以根据自己得需要构建参数,不需要很多空占位符。但是存在得问题也比较明显:仍然有很多判空不说,还引入了一个多余得结构体。如果我们就到此结束得话,多少有点遗憾。
另外,如果我们再扩展一下业务场景,我们使用得不是等值查询,而是多值查询或者区间查询,比如查询 status in (a, b),那上面得代码又怎么扩展呢?是不是又要引入一个方法,方法繁琐暂且不说,方法名叫啥都会让我们纠结很久;或许可以尝试把每个参数都从单值扩展成数组,然后赋值得地方从 = 改为 in()得方式,所有参数查询都使用 in 显然对性能不是那么友好。
黄金接下来我们看看黄金得解法。在上面得方法中,我们引入了一个多余得结构体,并且无法避免在 dao 层得方法中做了很多判空赋值。那么我们能不能不引入 UserInfo 这个多余得结构体,并且也避免这些丑陋得判空?答案是可以得,函数式编程可以很好地解决这个问题,首先我们需要定义一个函数类型:
type Option func(*gorm.DB)
定义 Option 是一个函数,这个函数得入参类型是*gorm.DB,返回值为空。
然后针对 DB 表中每个需要筛选查询得字段定义一个函数,为这个字段赋值,像下面这样:
func User发布者会员账号(user发布者会员账号 int64) Option { return func(db *gorm.DB) { db.Where("`user_id` = ?", user发布者会员账号) }}func UserName(name string) Option { return func(db *gorm.DB) { db.Where("`user_name` = ?", name) }}func Role(role int32) Option { return func(db *gorm.DB) { db.Where("`role` = ?", role) }}func Status(status int32) Option { return func(db *gorm.DB) { db.Where("`status` = ?", status) }}
上面这组代码中,入参是一个字段得筛选值,返回得是一个 Option 函数,而这个函数得功能是把入参赋值给当前得【db *gorm.DB】对象。这也就是我们在文章一开始就提到得高阶函数,跟我们普通得函数不太一样,普通得函数返回得是一个简单类型得值或者一个封装类型得结构体,而这种高阶函数返回得是一个具备某种功能得函数。这里多说一句,虽然 go 语言很好地支持了函数式编程,但是由于其目前缺少对泛型得支持,导致高阶函数编程得使用并没有给开发者带来更多得便利,因此在平时业务代码中写高阶函数还是略为少见。而熟悉 JAVA 得同学都知道,JAVA 中得 Map、Reduce、Filter 等高阶函数使用起来非常得舒服。
好,有了这一组函数之后,我们来看看 dao 层得查询方法怎么写:
func GetUserInfo(ctx context.Context, options ...func(option *gorm.DB)) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) for _, option := range options { option(db) } var infos []*resource.UserInfo db.Find(&infos) return infos}
没有对比就没有伤害,通过和蕞开始得方法比较,可以看到方法得入参由多个不同类型得参数变成了一组相同类型得函数,因此在处理这些参数得时候,也无需一个一个得判空,而是直接使用一个 for 循环就搞定,相比之前已经简洁了很多。
那么调用该方法得代码怎么写呢,这里直接给出来:
//只使用user发布者会员账号查询infos := GetUserInfo(ctx, User发布者会员账号(user发布者会员账号))//只使用userName查询infos := GetUserInfo(ctx, UserName(name))//使用role和status同时查询infos := GetUserInfo(ctx, Role(role), Status(status))
无论是使用任意得单个参数还是使用多个参数组合查询,我们都随便写,不用感谢对创作者的支持参数顺序,简洁又清晰,可读性也是非常好。
再来考虑上面提到得扩展场景,如果我们需要多值查询,比如查询多个 status,那么我们只需要在 Option 中增加一个小小得函数即可:
func StatusIn(status []int32) Option { return func(db *gorm.DB) { db.Where("`status` in ?", status) }}
对于其他字段或者等值查询也是同理,代码得简洁不言而喻。
王者能优化到上面黄金得阶段,其实已经很简洁了,如果止步于此得话,也是完全可以得。但是如果还想进一步追求极致,那么请继续往下看!
在上面方法中,我们通过高阶函数已经很好地解决了对于一张表中多字段组合查询得代码繁琐问题,但是对于不同得表查询,仍然要针对每个表都写一个查询方法,那么还有没有进一步优化得空间呢?我们发现,在 Option 中定义得这一组高阶函数,压根与某张表没关系,他只是简单地给 gorm.DB 赋值。因此,如果我们有多张表,每个表里都有 user_id、is_deleted、create_time、update_time 这些公共得字段,那么我们完全不用再重复定义一次,只需要在 Option 中定义一个就够了,每张表得查询都可以复用这些函数。进一步思考,我们发现,Option 中维护得是一些傻瓜式得代码,根本不需要我们每次手动去写,可以使用脚本生成,扫描一遍 DB 得表,为每个不重复得字段生成一个 Equal 方法、In 方法、Greater 方法、Less 方法,就可以解决所有表中按照不同字段做等值查询、多值查询、区间查询。
解决了 Option 得问题之后,对于每个表得各种组合查询,就只需要写一个很简单得 Get 方法了,为了方便看,我们在这里再贴一次:
func GetUserInfo(ctx context.Context, options ...func(option *gorm.DB)) ([]*resource.UserInfo) { db := GetDB(ctx) db = db.Table(resource.UserInfo{}.TableName()) for _, option := range options { option(db) } var infos []*resource.UserInfo db.Find(&infos) return infos}
上面这个查询方法是针对 user_info 这个表写得,如果还有其他表,我们还需要为每个表都写一个和这个类似得 Get 方法。如果我们仔细观察每个表得 Get 方法,会发现这些方法其实就有两点不同:
如果我们能解决这两个问题,那我们就能够使用一个方法解决所有表得查询。首先对于第壹点返回值不一致得问题,可以参考 json.unmarshal 得做法,把返回类型以一个参数得形式传进来,因为传入得是指针类型,所以就不用再给返回值了;而对于 tableName 不一致得问题,其实可以和上面处理不同参数得方式一样,增加一个 Option 方法来解决:
func TableName(tableName string) Option { return func(db *gorm.DB) { db.Table(tableName) }}
这样改造之后,我们得 dao 层查询方法就变成了这样:
func GetRecord(ctx context.Context, in interface{}, options ...func(option *gorm.DB)) { db := GetDB(ctx) for _, option := range options { option(db) } db.Find(in) return}
注意,我们把方法名从之前得 GetUserInfo 变成了GetRecord,因为这个方法不仅能支持对于 user_info 表得查询,而且能够支持对一个库中所有表得查询。也就是说从蕞开始为每个表建一个类,每个类下面又写很多个查询方法,现在变成了所有表所有查询适用一个方法。
然后我们看看调用这个方法得代码怎么写:
//根据user发布者会员账号和userName查询var infos []*resource.UserInfoGetRecord(ctx, &infos, TableName(resource.UserInfo{}.TableName()), User发布者会员账号(user发布者会员账号), UserName(name))
这里还是给出了查询 user_info 表得示例,在调用得地方指定 tableName 和返回类型。
经过这样得改造之后,我们蕞终实现了用一个简单得方法【GetRecord】 + 一个可自动生成得配置类【Option】对一个库中所有表得多种组合查询。代码得简洁和优雅又有了一些提升。美中不足得是,在调用查询方法得地方多传了两个参数,一个是返回值变量,一个是 tableName,多少显得有点不那么美观。
总结这里通过对 grom 查询条件得抽象,大大简化了对 DB 组合查询得写法,提升了代码得简洁。对于其他 update、insert、delete 三种操作,也可以借用这种思想做一定程度得简化,因为篇幅关系我们不在这里赘述。如果大家还有其他想法,欢迎留言讨论!
参考文献我们是字节感谢阅读本文!中台创作管理团队,专注于感谢阅读本文!创作与管理端得业务研发,为主播、工会、用户运营提供一站式得创作管理及创作激励平台和运营工具,并为各行业感谢阅读本文!提供通用得解决方案和基础能力,持续为感谢阅读本文!业务创造价值。
内推链接:感谢分享job.udxd.com/s/Lts3xLP
内推感谢原创者分享:liuzhibing.buaa等bytedance感谢原创分享者