抽象函数是Go语言的一等公民。将这种复杂的组合查询抽象成统一的方法和配置类,提高了代码的简洁和优雅,提高了开发人员的效率。后台有一个DB表。在业务中,需要根据这张表的不同字段进行过滤查询。这是一个非常普遍的要求。相信对于每一个做业务开发的人来说,这个需求都是不可避免的。比如我们有一张表,里面存放的是用户信息。简化表结构如下:CREATETABLE`user_info`(`id`bigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'自增主键',`user_id`bigintNOTNULLCOMMENT'用户id',`user_name`varcharNOTNULLCOMMENT'username',`role`intNOTNULLDEFAULT'0'COMMENT'role',`status`intNOTNULLDEFAULT'0'COMMENT'status',PRIMARYKEY(`id`),)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='用户信息表';这张表中有几个关键字段,user_id,user_name,role,status。如果我们要根据user_id进行过滤,一般会在dao层写这样一个方法(这里为了简洁,所有示例代码都省略了错误处理部分):funcGetUserInfoByUid(ctxcontext.Context,userIDint64)([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())varinfos[]*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())varinfos[]*resource.UserInfodb=db.Where("user_name=?",name)db.Find(&infos)returninfos}可以看到两个方法的代码极其相似。如果需要按角色或状态查询,就得多用几种方法,导致类似的方法很多。当然,我们很容易想到,我们可以通过一种方法,多输入几个参数来解决这个问题。因此,我们将以上两种方法合并为如下方法,可以支持基于多字段过滤查询:funcGetUserInfo(ctxcontext.Context,userIDint64,namestring,roleint,statusint)([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())varinfos[]*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}相应的,调用该方法的代码也需要改一下://OnlyqueryinfosbasedonUserID:=GetUserInfo(ctx,userID,"",0,0)//只根据用户名查询信息:=GetUserInfo(ctx,0,name,0,0)//只根据角色查询信息:=GetUserInfo(ctx,0,"",role,0)//只根据Status查询信息:=GetUserInfo(ctx,0,"",0,status)这样的代码,无论是对于per还是很不爽写代码的儿子和读代码的人。我们这里只列出四个参数。大家可以想一下,如果这张表中有十几二十个字段需要过滤查询的话,这段代码是什么样子的。首先,GetUserInfo方法本身有很多入参,里面充斥着各种!=0和!=”的判断,需要注意的是0一定不能作为字段的有效值,否则!=0的判断就会有问题。其次,作为调用者,明明只是根据一个字段来过滤查询,却要填一个0或者""让其他参数占位,调用者要特别谨慎,因为如果你是一不小心,你可能把role填成status因为他们的类型是一样的,编译器不会检查错误,很容易造成业务bug。解决方案如果在解决这类问题上有等级的话,那么上面的写法只能算是青铜级的。接下来,让我们看看白银、黄金和国王。银解决了这个问题。一种常见的解决方案是新建一个结构体,将所有的查询字段放在这个结构体中,然后将这个结构体作为入参传入dao层的查询方法中。.在调用dao方法的地方,根据自己的需要构造一个包含不同字段的结构体。在本例中,我们可以构建一个UserInfo结构体,如下所示:.Context,info*UserInfo)([]*resource.UserInfo){db:=GetDB(ctx)db=db.Table(resource.UserInfo{}.TableName())varinfos[]*resource.UserInfo如果信息。UserID>0{db=db.Where("user_id=?",info.UserID)}ifinfo.Name!=""{db=db.Where("user_name=?",info.Name)}ifinfo。角色>0{db=db.Where("role=?",info.Role)}ifinfo.Status>0{db=db.Where("status=?",info.Status)}db.Find(&infos)returninfos}相应的,调用该方法的代码也需要改一下://OnlyqueryinfobasedonuserD:=&UserInfo{UserID:userID,}infos:=GetUserInfo(ctx,info)//Onlyqueryinfobasedonname:=&UserInfo{Name:name,}infos:=GetUserInfo(ctx,info)这段代码写到这里,就是ac确实比原来的方法好多了。至少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)}varinfos[]*resource.UserInfodb.Find(&infos)returninfos}没有对比就没有伤害。通过与原方法的对比,可以看出该方法的入参从多个不同类型的参数变成了一组相同类型的函数,所以在处理这些参数的时候,有时候不需要判断一个空格一个空格,而是直接用一个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方法就可以对每个表进行各种组合查询了。为了方便查看,我们这里再贴一遍:=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和userName查询varinfos[]*resource.UserInfoGetRecord(ctx,&infos,TableName(resource.UserInfo{}.TableName()),UserID(userID),UserName(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
