函数是 Go 语言的一等公民,如何利用好其高级用法特性,是一件值得思考和实践的事情
背景
在日常业务开发中,对于一些表的不同字段做筛选查询,是基础的功能。而且大部分可能是在根据不同条件去查询。就像这样
代码语言:javascript复制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层也会有非常多的冗余代码,可能也就改变了一下入参而已。
假设有一张订单表,简化结构如下
代码语言:javascript复制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='订单表';
举例说明用以下这些字段的不同组合来查询
代码语言:javascript复制order_id, shop_id, produce_id,status
会在dao层来编写类似于这样的代码
根据orderId来查询
代码语言:javascript复制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来查询
代码语言:javascript复制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 查询,那需要再写几个类似的方法,导致相似的方法非常多。当然很容易想到,如果参数是传多个,传多个不就好了,可能就是这样的写法
代码语言:javascript复制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
}
如果什么时候业务有变化,需要改条件。也许就会变为这样
代码语言:javascript复制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
}
调用方的代码大概是这样的
代码语言:javascript复制// 根据shopId 查询
infos := GetOrderInfo(ctx, 0, 1)
// 根据orderId 查询
infos := GetOrderInfo(ctx, 1, 0)
相当于其他不关心的查询字段用对应类型默认的零值来替换了。
当然也可以用结构体来作为一个参数
代码语言:javascript复制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 官方有这样一句话
代码语言:javascript复制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或者像以下这种方式来判断
代码语言:javascript复制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判断。
基于以上这种所有情况,有必要来优化一下
可以利用函数式编程来优化
定义如下
代码语言:javascript复制type Option func(*gorm.DB)
定义 Option 是一个函数,这个函数的入参类型是*gorm.DB,返回值为空。
然后针对 表中需要筛选查询的字段定义一个函数,赋值
代码语言:javascript复制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层就很方便了。
代码语言:javascript复制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循环来代替
调用者知道自己需要根据什么参数来查询,则就用上面写好的参数函数来作为入参
代码语言:javascript复制// orderID 查询
infos := GetOrderInfo(ctx, OrderID(orderID))
// orderID,shopID 组合查询
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