Kratos框架单体应用实战二


前言

本篇文章讲述下在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层权重处理问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值