函数是 Go 语言的一等公民,如何利用好其高级用法特性,是一件值得思考和实践的事情
背景 在日常业务开发中,对于一些表的不同字段做筛选查询,是基础的功能。而且大部分可能是在根据不同条件去查询。就像这样
1 2 3 4 5 6 type XXXRepo interface { GetXXXByIdOrName(ctx context.Context, id int , name string ) (o []admin.XXX, err error ) GetXXXInfoList(ctx context.Context, req *GetXXXRequest) (total int64 , o []admin.XXX, err error ) GetXXXInfo(ctx context.Context, columnId, gradeId int ) (o []admin.XXX, err error ) GetXXXByIdList(ctx context.Context, idList []int ) (o []admin.XXX, err error ) }
这也还只是少许的一些条件,如果一张表有十多个字段配合查询呢 ? dao层也会有非常多的冗余代码,可能也就改变了一下入参而已。
假设有一张订单表,简化结构如下
1 2 3 4 5 6 7 8 CREATE TABLE `order` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键' , `order_id` bigint NOT NULL COMMENT '订单id' , `shop_id` varchar NOT NULL COMMENT '店铺id' , `product_id` int NOT NULL DEFAULT '0' COMMENT '商品id' , `status` int NOT NULL DEFAULT '0' COMMENT '状态' , PRIMARY KEY (`id` ), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表' ;
举例说明用以下这些字段的不同组合来查询
1 order_id, shop_id, produce_id,status
会在dao层来编写类似于这样的代码
根据orderId来查询
1 2 3 4 5 6 7 8 func GetOrderInfoByOrderId (ctx context.Context, orderId int64 ) ([]*resource.Order) { db := GetDB(ctx) db = db.Table(resource.Order{}.TableName()) var infos []*resource.Order db = db.Where("order_id = ?" , orderId) db.Find(&infos) return infos }
根据shopId来查询
1 2 3 4 5 6 7 8 func GetOrderInfoByShopId (ctx context.Context, shopId int64 ) ([]*resource.Order) { db := GetDB(ctx) db = db.Table(resource.Order{}.TableName()) var infos []*resource.Order db = db.Where("shop_id = ?" , shopId) db.Find(&infos) return infos }
可以看到,两个方法的代码极度相似 ,除了入参和命名不一样,如果再需要按照 produce_id 或者 status 查询,那需要再写几个类似的方法,导致相似的方法非常多。当然很容易想到,如果参数是传多个,传多个不就好了,可能就是这样的写法
1 2 3 4 5 6 7 8 func GetOrderInfo (ctx context.Context,orderId, shopId int64 ) ([]*resource.Order) { db := GetDB(ctx) db = db.Table(resource.Order{}.TableName()) var infos []*resource.Order db = db.Where("shop_id = ? and order_id = ?" , shopId,orderId) db.Find(&infos) return infos }
如果什么时候业务有变化,需要改条件。也许就会变为这样
1 2 3 4 5 6 7 8 9 10 11 12 13 func GetOrderInfo (ctx context.Context,orderId, shopId int64 ) ([]*resource.Order) { db := GetDB(ctx) db = db.Table(resource.Order{}.TableName()) var infos []*resource.Order if orderId != 0 { db = db.Where("order_id = ?" ,orderId) } if shopId != 0 { db = db.Where("shop_id = ?" ,shopId) } db.Find(&infos) return infos }
调用方的代码大概是这样的
1 2 3 4 5 infos := GetOrderInfo(ctx, 0 , 1 ) infos := GetOrderInfo(ctx, 1 , 0 )
相当于其他不关心的查询字段用对应类型默认的零值来替换了。
当然也可以用结构体来作为一个参数
1 2 3 4 5 6 func GetOrderInfo (ctx context.Context,order Order) ([]*resource.Order) { db := GetDB(ctx) db = db.Table(resource.Order{}.TableName()) db.Where(&order).find(&infos) return infos }
但是 估计有的人遇到过这样的坑,那就是如果当字段是int,int64等,有0时,不清楚到底是传入了0,还是没有传值,是区分不了的。因为go语言默认的类型零值。 如果是类型的0值也想作为参数来查询,则默认是忽略的,可以参考
gorm 官方有这样一句话
1 NOTE When querying with struct, GORM will only query with non-zero fields, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions, for example:
针对这种情况可以选择转化为map或者像以下这种方式来判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func GetOrderInfoInfo (ctx context.Context, o Order) ([]*resource.Order) { db := GetDB(ctx) db = db.Table(resource.Order{}.TableName()) var infos []*resource.Order if o.orderId > 0 { db = db.Where("order_id = ?" , o.orderId) } if o.shopId != "" { db = db.Where("shop_id = ?" , o.shop_id) } if xxxx db.Find(&infos) return infos }
这里还只是简短几个字段,如果是十几个字段来组合查询,则要写非常多if判断。
基于以上这种所有情况,有必要来优化一下
可以利用函数式编程 来优化
定义如下
1 type Option func (*gorm.DB)
定义 Option 是一个函数,这个函数的入参类型是*gorm.DB,返回值为空。
然后针对 表中需要筛选查询的字段定义一个函数,赋值
1 2 3 4 5 6 7 8 9 10 11 12 func OrderID (orderID int64 ) Option { return func (db *gorm.DB) { db.Where("`order_id` = ?" , userID) } } func ShopID (shopID int64 ) Option { return func (db *gorm.DB) { db.Where("`shop_id` = ?" , shopID) } }
所以需要为可能得字段来创建不同的函数,返回一个Option函数,该函数是把入参赋值给【db *gorm.DB】对象
所以基于以上,要改写dao层就很方便了。
1 2 3 4 5 6 7 8 9 10 func GetOrderInfo (ctx context.Context, options ...func (option *gorm.DB) ) ([]*resource.OrderInfo) { db := GetDB(ctx) db = db.Table(resource.OrderInfo{}.TableName()) for _, option := range options { option(db) } var infos []*resource.OrderInfo db.Find(&infos) return infos }
这样底层的逻辑就不用写很多if判断了,用 for循环来代替
调用者知道自己需要根据什么参数来查询,则就用上面写好的参数函数来作为入参
1 2 3 4 5 infos := GetOrderInfo(ctx, OrderID(orderID)) infos := GetOrderInfo(ctx, OrderID(orderID), ShopID(shopID))
当然还根据其他 in 等条件查询,再写一个函数即可
经过优化之后,简化了逻辑。相当于配置类的Option就生成了,代码优雅了不少。这里只提到了查询,更新也是类似的道理,删除和写入就没太大必要这样了。
参考Self-referential functions and the design of options
Using functional options instead of method chaining in Go