抽象函数是Go语言的一等公民。将这种复杂的组合查询抽象成统一的方法和配置类,提高了代码的简洁和优雅,提高了开发人员的效率。后台有一个DB表。在业务中,需要根据这张表的不同字段进行过滤查询。这是一个非常普遍的要求。相信对于每一个做业务开发的人来说,这个需求都是不可避免的。比如我们有一张表,里面存放的是用户信息。简化表结构如下:CREATETABLE`user_info`(`id`bigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'自增主键',`user_id`bigintNOTNULLCOMMENT'用户id',`user_name`varcharNOTNULLCOMMENT'用户名',`role`intNOTNULLDEFAULT'0'COMMENT'role',`status`intNOTNULLDEFAULT'0'COMMENT'status',PRIMARYKEY(`id`),)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='用户信息表';这个表里面有几个Key字段,user_id,user_name,role,status。如果我们要根据user_id进行过滤,那么我们一般会在dao层写这样一个方法(为了简洁,这里所有示例代码都省略了错误处理部分):funcGetUserInfoByUid(ctxcontext.Context,userIDint64)([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())variinfos[]*resource.UserInfodb=db.Where("user_id=?",userID)db.Find(&infos)returninfos}如果业务需要通过user_name查询,那么我们需要写一个类似的方法通过user_name查询:funcGetUserInfoByName(ctxcontext.Context,namestring)([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())variinfos[]*resource.UserInfodb=db.Where("user_name=?",name)db.Find(&infos)returninfos}可以看到这两种方法的代码非常相似。如果需要按角色或状态查询,就得想出好几种方法,导致类似的方法很多。当然,我们很容易想到,我们可以通过一种方法,多输入几个参数来解决这个问题。因此,我们将上述两种方法组合成如下方法,可以支持基于多字段过滤查询:(ctx)db=db.Table(resource.UserInfo{}.TableName())variinfos[]*resource.UserInfoifuserID>0{db=db.Where("user_id=?",userID)}ifname!=""{db=db.Where("user_name=?",name)}ifrole>0{db=db.Where("role=?",role)}ifstatus>0{db=db.Where("status=?",status)}db.Find(&infos)returninfos}相应的,调用该方法的代码也需要改一下://只根据UserID查询infos:=GetUserInfo(ctx,userID,"",0,0)//仅根据UserName查询信息:=GetUserInfo(ctx,0,name,0,0)//仅根据Role查询信息:=GetUserInfo(ctx,0,"",role,0)//仅根据Role查询信息Status:=GetUserInfo(ctx,0,"",0,status)这种代码无论是写代码的人还是看代码的人,都会觉得很不爽。我们这里只列出四个参数。大家可以想一下,如果这张表中有十几二十个字段需要过滤查询的话,这段代码是什么样子的。首先,GetUserInfo方法本身有很多入参,里面充斥着各种!=0和!=”的判断,需要注意的是0一定不能作为字段的有效值,否则!=0的判断就会有问题。其次,作为调用者,明明只是根据一个字段来过滤查询,却要填一个0或者""让其他参数占位,调用者要特别谨慎,因为如果你是一不小心,你可能把role填成status因为他们的类型是一样的,编译器不会检查错误,很容易造成业务bug。解决方案如果在解决这类问题上有等级的话,那么上面的写法只能算是青铜级的。接下来,让我们看看白银、黄金和国王。银解决了这个问题。一种常见的解决方案是新建一个结构体,将所有的查询字段放在这个结构体中,然后将这个结构体作为入参传入dao层的查询方法中。.在调用dao方法的地方,根据自己的需要构造一个包含不同字段的结构体。在这个例子中,我们可以构建一个UserInfo结构体,如下所示:resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())variinfos[]*resource.UserInfoifinfo.UserID>0{db=db.Where("user_id=?",info.UserID)}ifinfo.Name!=""{db=db.Where("user_name=?",info.Name)}ifinfo.Role>0{db=db.Where("role=?",info.Role)}ifinfo.Status>0{db=db.Where("status=?",info.Status)}db.Find(&infos)returninfos}相应的,调用该方法的代码也需要改一下://只根据userD查询信息:=&UserInfo{UserID:userID,}infos:=GetUserInfo(ctx,info)//只根据name查询信息:=&UserInfo{Name:name,}infos:=GetUserInfo(ctx,info))这个这里写的代码其实比原来的方法好多了。至少dao层的方法从输入参数多变成了一个。调用者的代码也可以根据自己的需要构造参数,不需要很多空格。占位符。但是,存在的问题也很明显:还是有很多空判断,引入了冗余结构。如果我们就此结束,那将是一个遗憾。另外,如果我们扩展业务场景,不使用等值查询,而是多值查询或者区间查询,比如查询(a,b)中的状态,上面的代码如何扩展呢?是不是有必要再介绍一个方法,方法繁琐不说,方法名会让我们纠结半天;或许我们可以尝试将每个参数从单个值扩展为数组,然后将赋值从=改为in()这样,对所有参数查询都使用in显然对性能不是那么友好。黄金接下来我们看看黄金的解决方案。在上面的方法中,我们引入了一个冗余的结构,在dao层方法中难免要做大量的空判断赋值。那么能不能不引入UserInfo的冗余结构,避免这些丑陋的空判断呢?答案是肯定的。函数式编程可以很好的解决这个问题。首先,我们需要定义一个函数类型:typeOptionfunc(*gorm.DB)定义Option是一个函数。该函数的入参类型为*gorm.DB,返回值为空。然后为DB表中每个需要过滤查询的字段定义一个函数,并为该字段赋值,如下:funcUserID(userIDint64)Option{returnfunc(db*gorm.DB){db.Where("`user_id`=?",userID)}}funcUserName(namestring)Option{returnfunc(db*gorm.DB){db.Where("`user_name`=?",name)}}funcRole(roleint32)Option{returnfunc(db*gorm.DB){db.Where("`role`=?",role)}}funcStatus(statusint32)Option{returnfunc(db*gorm.DB){db.Where("`status`=?",status)}}上面这组代码中,入参是一个字段的过滤值,返回的是一个Option函数,这个函数的作用是将入参赋值给当前的[db*gorm.DB]对象。这就是我们在文章开头提到的高阶函数。它不同于我们普通的函数。普通函数返回的是简单类型的值或者封装类型的结构,而这个高阶函数返回的是一个做某事的函数。这里还有一点,虽然go语言对函数式编程支持的很好,但是由于其目前对泛型的支持不足,高阶函数式编程的使用并没有给开发者带来更多的方便,所以在平时写高阶业务代码中的功能仍然很少见。熟悉JAVA的同学都知道,JAVA中的Map、Reduce、Filter等高阶函数用起来是非常舒服的。好了,有了这组函数,我们来看看dao层的查询方法怎么写:funcGetUserInfo(ctxcontext.Context,options...func(option*gorm.DB))([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())for_,option:=rangeoptions{option(db)}variinfos[]*resource.UserInfodb.Find(&infos)returninfos}None比较没有坏处。通过与原始方法的对比,我们可以看到该方法的输入参数从多个不同类型的参数变成了一组相同类型的函数。所以在处理这些参数的时候,不需要判断空,而是直接用一个for循环搞定,比以前简单多了。那么调用这个方法的代码怎么写,这里直接给出://OnlyuseuserIDtoqueryinfos:=GetUserInfo(ctx,UserID(userID))//OnlyuseuserNametoqueryinfos:=GetUserInfo(ctx,UserName(name))//同时使用角色和状态查询信息:=GetUserInfo(ctx,Role(role),Status(status))无论是使用任何单个参数还是使用多个参数的组合来查询,我们可以随便写,不用注意参数的顺序,简洁明了,可读性很强。考虑上面提到的扩展场景。如果我们需要多值查询,比如查询多个状态,那么只需要在Option中添加一个小函数即可:funcStatusIn(status[]int32)Option{returnfunc(db*gorm.DB){db.Where("`status`in?",status)}}对于其他字段或等价查询也是一样的,代码的简洁性不言而喻。将王者优化到上面的黄金境界,其实很简单。如果就此止步,完全有可能。但如果你想更进一步追求极致,请继续往下看!在上面的方法中,我们通过高阶函数解决了一个表中多字段组合查询代码繁琐的问题,但是对于不同的表查询,我们还是需要为每个表写一个查询方法,那么有没有没有进一步优化的空间?我们发现Option中定义的那组高阶函数与一张表完全没有关系,它只是简单的给gorm.DB赋值。因此,如果我们有多个表,并且每个表都有user_id、is_deleted、create_time、update_time等公共字段,那么我们根本不需要重复定义,只需要在Option中定义一个,每个表查询可以重用这些功能。进一步思考后,我们发现Option维护了一些傻瓜式的代码。我们不需要每次都手动编写它们。我们可以使用脚本来生成它们,扫描数据库表,并为每个非重复字段生成一个Equal方法。In方法、Greater方法、Less方法可以根据所有表的不同字段解决等值查询、多值查询、区间查询。解决了Option问题后,只需要写一个非常简单的Get方法就可以对每个表进行各种组合查询。为了方便查看,我们这里再贴一遍:funcGetUserInfo(ctxcontext.Context,options...func(option*gorm.DB))([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())for_,option:=rangeoptions{option(db)}varinfos[]*resource.UserInfodb.Find(&infos)returninfos}上面的查询方法是针对user_info表写的.如果还有其他的表,我们需要为每张表写一个类似这样的Get方法。如果我们仔细观察各个表的Get方法,就会发现这些方法其实有两点不同:返回值类型不同;表名不同。如果我们能解决这两个问题,那么我们就可以用一种方法来解决所有表的查询。首先,对于第一点的返回值不一致,可以参考json.unmarshal的方法,将返回类型作为参数传入。因为传入的是指针类型,所以不需要给出返回值;而对于tableName不一致的问题,其实可以像上面一样通过增加一个Option方法来处理不同的参数来解决:funcTableName(tableNamestring)Option{returnfunc(db*gorm.DB){db.Table(tableName)}}这样改造之后,我们的dao层查询方法就变成了这样:funcGetRecord(ctxcontext.Context,ininterface{},options...func(option*gorm.DB)){db:=GetDB(ctx)for_,option:=rangeoptions{option(db)}db.Find(in)return}注意我们把方法名从之前的GetUserInfo改成了GetRecord,因为这个方法不仅可以支持对user_info表的查询,还可以支持对所有表的查询图书馆。也就是说一开始就为每个表创建一个类,在每个类下写很多查询方法。现在它已经成为一种适用于所有表的所有查询的方法。那我们看看调用这个方法的代码怎么写://根据userID查询variinfos[]*resource.UserInfoGetRecord(ctx,&infos,TableName(resource.UserInfo{}.TableName()),UserID(userID),UserNameanduserName(name))这里是查询user_info表的例子,调用处指定tableName和返回类型。经过这样的改造,我们最终通过一个简单的方法【GetRecord】+一个自动生成的配置类【Option】,实现了一个库中所有表的多种组合查询。代码的简洁性和优雅性得到了一些改进。美中不足的是在调用查询方法的地方多传了两个参数,一个是返回值变量,一个是tableName,有些不美观。总结这里,grom查询条件的抽象,大大简化了DB组合查询的编写,提高了代码的简洁性。对于其他的update、insert、delete三个操作,也可以采用这种思路进行一定程度的简化,限于篇幅,这里不再赘述。如果您有其他想法,欢迎留言讨论!参考资料https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.htmlhttps://coolshell.cn/articles/21146.html
