文章目录
前言
本篇文章讲述下在kratos-use目录结构中如何进行单体应用的开发,关于环境搭建和项目运行等相关说明请参考 Kratos框架单体应用实战一
一、配置的读取
配置相关的读取是通过环境变量的方式,例如数据库连接串,日志文件的存储路径,用户登录jwt密钥等。环境变量的key以及填充单独放在env.go中,根据需要进行添加新的变量。读取之后填充到Bootstrap启动项配置中,方便后续读取。
二、api层的接口定义
根据业务中新需求,在开发流程中在进行需求分析后,首先进行api接口的定义(方便前端同学进行开发,不被阻塞)和数据库表的设计(如何需要增加新的表结构或字段)。首先接口的定义都在api文件夹下,按照业务模块创建对应的文件夹,例如用户模块(user),日记模块(diary),如果希望面向APP端和面向web端的接口互不影响,最好分开两个文件夹,将接口处理逻辑分开,这样就不必担心改了web的接口影响app端,缺点就是多了很多冗余的代码(也是可以接受的)或者每次更改接口将所有涉及的应用都测试一遍,主要还是进行取舍。
用例–用户模块
在user/v1文件夹下创建user.proto文件,添加注册,登录,获取登录的用户信息三个接口定义。接口定义的风格可以使用RESTful风格,也可以统一都用post方式。命名规则没有特别的要求,统一就行。
接口文件定义好后,执行make api命令自动生成http和grpc代码,如果不需要生成grpc代码可以在Makefile中的api命令节点下的go-grpc_out去掉,此时api层就已经完成。
三、数据库表的设计
定义api接口和表的设计不分先后,我一般先定义api接口再设计表结构,因为接口是面向需求和前端的,在定义接口的过程中可以更加掌握需求,进而更好的进行表的设计。
我这边使用的orm库是ent,你也可以使用gorm等其他流行的开源库。
1 实体创建
在根目录执行命令 ent new User创建用户实体,其会在根目录生成ent/schema/user.go代码。你可以在里面添加所需要的字段并设计字段属性。关于索引等属性的设计可以参考ent官方文档提供的指导。
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
fields := []ent.Field{
field.String("account").Default("").Comment("用户账号"),
field.String("password").Default("").Comment("密码"),
field.String("password_salt").Default("").Comment("加盐"),
field.String("name").Default("unknown").Comment("用户名称"),
field.String("mobile").Default("").Comment("手机号"),
}
return append(BaseFields, fields...)
}
2 公共字段抽离
我这边将公共的数据库字段如id,created_at,updated_at等字段抽出来放在了base.go文件中,方便共享和维护
var (
BaseFields = []ent.Field{
field.String("id").
MaxLen(60).
NotEmpty().
Unique().
Immutable().DefaultFunc(func() string {
id, _ := uuid.NewUUID()
return id.String()
}),
field.Int64("created_at").Default(0),
field.Int64("updated_at").Default(0),
//field.Int64("deleted_at").Default(0),
}
)
3 软删除实现
为了实现软删除操作,ent需要单独做个Hooks处理,可以参考ent官方文档,或者查看 我的实现 基本都差不多。
4 数据库连接
我在schema中创建了一个dbo的文件夹,用来编写关于数据库连接和配置相关的代码,目前kratos-use项目中只用到了mysql。下述代码中
使用到了ent的修改器Mutator概念,需要拦截数据库操作CURD时对相应的字段做公共修改,不需要每次都手动赋值。
func Open(source string) (*ent.Client, error) {
drv, err := sql.Open("mysql", source)
if err != nil {
return nil, err
}
log.Info("mysql连接成功")
// 获取数据库驱动中的sql.DB对象。
db := drv.DB()
db.SetMaxIdleConns(10)
db.SetMaxOpenConns(100)
db.SetConnMaxLifetime(time.Hour)
client := ent.NewClient(ent.Driver(drv), ent.Debug())
client.Intercept(
intercept.Func(func(ctx context.Context, q intercept.Query) error {
if ent_o.QueryFromContext(ctx).Limit == nil {
q.Limit(500)
}
return nil
}),
)
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
start := time.Now()
defer func() {
log.Context(ctx).Infof("数据库操作日志: Op=%s\tType=%s\tTime=%s\tConcreteType=%T\n", m.Op(), m.Type(), time.Since(start), m)
}()
switch m.Op() {
case ent.OpCreate:
err := m.SetField("created_at", time.Now().Unix())
if err != nil {
log.Context(ctx).Errorw("msg", "设置创建时间字段失败", "err", err)
return nil, err
}
case ent.OpUpdateOne, ent.OpUpdate:
err := m.SetField("updated_at", time.Now().Unix())
if err != nil {
log.Context(ctx).Errorw("msg", "设置更新时间字段失败", "err", err)
return nil, err
}
case ent.OpDeleteOne, ent.OpDelete:
err := m.SetField("deleted_at", time.Now().Unix())
if err != nil {
log.Context(ctx).Errorw("msg", "设置删除时间字段失败", "err", err)
return nil, err
}
}
return next.Mutate(ctx, m)
})
})
return client, nil
}
四、data层
数据库实体设计完成,mysql连接方法配置完成后,就可以引入到data层,完成方法调用。这部分的代码在/internal/data/data.go中,一般缓存,数据库都在这里构建。其中调用ent的Schema.Create方法可以实现表的自动创建。
// NewDB 数据库
func NewDB(bootstrap *conf.Bootstrap) (*ent.Client, error) {
c := bootstrap.Data
client, err := mysql.Open(c.Database.Source)
if err != nil {
log.Infow("msg", "open mysql error", "err", err)
return nil, err
}
// 自动迁移
if err := client.Schema.Create(context.Background()); err != nil {
log.Infow("msg", "failed creating schema resources", "err", err)
return nil, err
}
return client, nil
}
然后我们在data文件夹下创建一个user.go用来编写用户模块在data层的业务逻辑。具体要实现哪些方法是在biz层定义的,从而实现依赖倒置。
type userRepo struct {
data *Data
log *log.Helper
}
// NewUserRepo .
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
return &userRepo{
data: data,
log: log.NewHelper(logger),
}
}
五、biz层
该层很纯净,不会依赖其他的层,repo的接口也是在这里定义,data层中的user.go实现这个repo即可。
// UserRepo
type UserRepo interface {
CreateForRegister(ctx context.Context, input *entity.RegisterInput) (string, error)
GetUserById(ctx context.Context, id string) (*entity.UserDTO, error)
GetUserByAccount(ctx context.Context, id string) (*entity.UserDTO, error)
}
该层拥有自己的实体对象,我在该层主要以input,output结尾分别代表输入输出的类型,有些实体也会用DTO结尾代表其仅仅是承载data层返回的内容。优点是service层和data层的代码修改不会造成biz层的变动,并且很方便的对biz层做单元测试。
六、service层
该层实现了 api 定义的服务层,我们可以创建user.go文件,并且添加UserService服务对象,然后实现注册,登录等api接口
type UserService struct {
v1.UnimplementedUserServer
userUseCase *biz.UserUsecase
}
func NewUserService(userUseCase *biz.UserUsecase) *UserService {
return &UserService{
userUseCase: userUseCase,
}
}
服务编写完成需要在server/http.go文件下进行注册,才能生效。
七、其他一些整合
为了方便维护service层,biz层,data层中定义的结构体的创建工作,引入了 wire工具,他是一个灵活的依赖注入工具,通过自动生成代码的方式在编译期完成依赖注入。
// ProviderSet is service providers.
var ProviderSet = wire.NewSet(
NewCommonService,
NewUserService,
NewDiaryService,
)
像上述形式,然后执行make wire,就可以在cmd/kratos_use下自动生成代码,不需要人工去关心每个对象的依赖情况。
总结
至此就已完成了框架的大致介绍,配置好环境变量,就可以执行kratos run运行服务。后续我会说下数据库事务的引入以及biz和data层权重处理问题。