这节我以一个简单的创建订单功能为例,把逻辑分层解藕的方法论用实际代码再讲解一遍。
图片
演示按照可能是多数人的一个开发习惯:先定义好Model 、请求、响应等数据对象,再按照自底向上的顺序即--DAL->领域服务->应用服务->控制器的顺序进行代码编写。
数据对象
model
先从Model开始,首先在dal/model 目录下创建demo.go ,因为还没有真正开发进行需求的开发,仍然算项目搭建过程中的测试代码,所以我们把文件命名成了demo.go。
type DemoOrder struct {
Id int64 `gorm:"column:id;primary_key" json:"id"` //自增ID
UserId int64 `gorm:"column:user_id" json:"user_id"` //用户ID
BillMoney int64 `gorm:"column:bill_money" json:"bill_money"` //订单金额(分)
OrderNo string `gorm:"column:order_no;type:varchar(32)" json:"order_no"` //订单号
State int8 `gorm:"column:state;default:1" json:"state"` //1-待支付,2-支付成功,3-支付失败
PaidAt time.Time `gorm:"column:paid_at;default:\"1970-01-01 00:00:00\"" json:"paid_at"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` //创建时间
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` //更新时间
}
这里要说一下 IsDel 这个字段,这个字段被设置成了soft_delete.DeletedAt 类型。这个是GORM V2 中新增的特性让软删除字段支持更多类型,在V1中软删除字段必须命名成deleted_at 并且字段在数据库中的默认值是NULL。
这在很多公司里DBA设置的约束里是不允许的,所以我之前没有使用过。但是现在GORM V2 支持Flag 模式了,就是咱们很多人用的0代表未删除 1代表删除,那么这个特性就可以应用起来了。
使用前需要先安装GORM的soft_delete这个包。
go get -u "gorm.io/plugin/soft_delete"
在定义模型时给字段设置其类型和Tag标签
type DemoOrder struct {
...
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
...
}
那么这样GORM在执行SQL语句时就会自动带上is_del这个字段进行查询啦
// Query
SELECT * FROM demo_orders WHERE is_del = 0;
// Delete
UPDATE demo_orders SET is_del = 1 WHERE id = 1;
领域对象
然后是领域对象,在logic/do 目录中新建 demo.go 文件,在其中定义DemoOrder领域对象
type DemoOrder struct {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
BillMoney int64 `json:"bill_money"`
OrderNo string `json:"order_no"`
State int8 `json:"state"`
IsDel uint `json:"is_del"`
PaidAt time.Time `json:"paid_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
可以看到这个领域对象和Model对象没啥区别,确实是这样的,如果Model的字段都有业务意义那字段基本上完全一样,对于只针对数据库有意义的非业务字段就没必要出现在领域对象中了。
响应对象
响应对象是针对客户端需求的,比如像ID这种在业务内部才有意义的字段可以选择不暴露出去,只通过orderNo之类的标识请求后端接口就可以了。
在 api/reply 目录下我们新建demo.go 并创建响应对象,其跟领域对象的区别是少了id、is_del这种客户端不需要知道的字段,以及把时间的类型都换成了字符串,我们在创建响应对象时把订单中的各种时间格式化成字符串再赋给响应对象,这样控制器拿到响应对象后直接返回就可以啦。
type DemoOrder struct {
UserId int64 `json:"user_id"`
BillMoney int64 `json:"bill_money"`
OrderNo string `json:"order_no"`
State int8 `json:"state"`
PaidAt string `json:"paid_at"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
请求对象
此外我们还在 api/request 目录下的demo.go 中定义了创建订单的请求对象
type DemoOrderCreate struct {
UserId int64 `json:"user_id"`
BillMoney int64 `json:"bill_money" binding:"required"`
// 这个字段演示的时候因为没创建订单快照表所以不写库
OrderGoodsId int64 `json:"order_goods_id" binding:"required"`
}
在Controller接收到请求后,它会利用Gin提供的数据验证和绑定帮我们验证请求数据然后把它们绑定到请求对象上。
Copier
这里我们先暂停一下, 很多人可能会有疑问你搞那么多对象,到时候得多写多少代码呀?
那么这里我就介绍一下这个工具"github.com/jinzhu/copier",也是GORM的作者开发的,它的作用类似于Java的BeanUtils.copyProperties 把源对象中的字段拷贝到目标对象中去。
我在项目common/util/copy.go中封装了一个工具函数帮我们完成数据拷贝,同时还定义了从时间对象转换成时间字符串的转换器,让我们在拷贝数据的同时完成time.Time类型字段的格式化。这样从领域对象转换成返回给客户端使用的响应对象的时就不需要再手动转换了。
使用我们的数据转换工具util.CopyProperties后上面的代码可以直接简化成下图这样
图片
使用util.CopyProperties即可完成数据对象的转换,不需要我们在一个字段一个字段的去复制了,也省去了经常做的时间转换的操作。
这个工具必不可少的会使用反射来完成数据复制,如果对性能很敏感,可以自己写一些领域对象到响应对象的Convertor方法,如果量大嫌自己写的麻烦,可以研究一下用Go编译的AST ,在编译时自动生成这些Convertor方法。