写点什么

Golang 微服框架 Kratos 与它的小伙伴系列 - ORM 框架 - Ent

作者:喵个咪
  • 2023-07-27
    湖南
  • 本文字数:11573 字

    阅读完需:约 38 分钟

Golang 微服框架 Kratos 与它的小伙伴系列 - ORM 框架 - Ent

什么是 ORM?

面向对象编程和关系型数据库,都是目前最流行的技术,但是它们的模型是不一样的。


面向对象编程把所有实体看成对象(object),关系型数据库则是采用实体之间的关系(relation)连接数据。很早就有人提出,关系也可以用对象表达,这样的话,就能使用面向对象编程,来操作关系型数据库。


简单说,ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。


ORM 把数据库映射成对象。


  • 数据库的表(table) --> 类(class)

  • 记录(record,行数据)--> 对象(object)

  • 字段(field)--> 对象的属性(attribute)


举例来说,下面是一行 SQL 语句。


SELECT id, first_name, last_name, phone, birth_date, sexFROM persons WHERE id = 10
复制代码


程序直接运行 SQL,操作数据库的写法如下。


res = db.execSql(sql);name = res[0]["FIRST_NAME"];
复制代码


改成 ORM 的写法如下。


p = Person.get(10);name = p.first_name;
复制代码


一比较就可以发现,ORM 使用对象,封装了数据库操作,因此可以不碰 SQL 语言。开发者只使用面向对象编程,与数据对象直接交互,不用关心底层数据库。


ORM 有下面这些优点:


  • 数据模型都在一个地方定义,更容易更新和维护,也利于重用代码。

  • ORM 有现成的工具,很多功能都可以自动完成,比如数据消毒、预处理、事务等等。

  • 它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。

  • 基于 ORM 的业务代码比较简单,代码量少,语义性好,容易理解。

  • 你不必编写性能不佳的 SQL。


ORM 也有很突出的缺点:


  • ORM 库不是轻量级工具,需要花很多精力学习和设置。

  • 对于复杂的查询,ORM 要么是无法表达,要么是性能不如原生的 SQL。

  • ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。

什么是 Ent?

ent 是 Facebook 开源的一个简单但是功能强大的 ORM 框架,它可以轻松构建和维护具有大型数据模型的应用程序。它基于代码生成,并且可以很容易地进行数据库查询以及图遍历。


它具有以下的特点:


  • 简单地使用数据库结构作为图结构。

  • 使用 Go 代码定义结构。

  • 基于代码生成的静态类型。

  • 容易地进行数据库查询和图遍历。

  • 容易地使用 Go 模板扩展和自定义。

  • 多存储驱动程序 - 支持 MySQL、PostgreSQL、SQLite 和 Gremlin。

如何去学习 Ent?

想要上手 ent,需要学习和了解三个方面:


  1. entc

  2. Schema

  3. CURD API


Ent 因为是基于代码生成的,所以,首当其冲的,自然是要去了解其 CLI 工具,没有它,如何去生成代码?


其次就是生成代码的模板:Schema。它主要是定义了表结构信息,至关重要的核心信息。生成数据库的结构和操作代码需要它,生成 gRPC 和 GraphQL 的接口也还是需要它。没它不行。


最后,就是学习使用一些数据库的基本操作,比如:连接数据库,CURD API。


从此往后,你就能够使用 ent 愉快的开始工作了。

CLI 工具

使用以下命令安装 entc 工具:


go install entgo.io/ent/cmd/ent@latest
复制代码

Schema

Schema 相当于数据库的表。


《道德经》说:


道生一,一生二,二生三,三生万物。


Schema,就是一切的起始点。


只有定义了 Schema,CLI 才能够生成数据库表的结构和操作的相关代码,有了相关代码,才能够操作数据库表的数据。


后面想要生成 gRPC 和 GraphQL 的接口定义,也还是需要 Schema。

创建一个 Schema

创建 Schema 有两个方法可以做到:

使用 entc init 创建

ent init User
复制代码


将会在 {当前目录}/ent/schema/ 下生成一个user.go文件,如果没有文件夹,则会创建一个:


package schema
import "entgo.io/ent"
// User holds the schema definition for the User entity.type User struct { ent.Schema}
// Fields of the User.func (User) Fields() []ent.Field { return nil}
// Edges of the User.func (User) Edges() []ent.Edge { return nil}
复制代码

SQL 转换 Schema 在线工具

网上有人好心的制作了一个在线工具,可以将 SQL 转换成 schema 代码,实际应用中,这是非常方便的!


SQL 转 Schema 工具: https://printlove.cn/tools/sql2ent


比如,我们有一个创建表的 SQL 语句:


CREATE TABLE `user`  (`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,`type` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,`created_at` timestamp NULL DEFAULT NULL,`updated_at` timestamp NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = DYNAMIC;
复制代码


转换之后,生成如下的 Schema 代码:


package schema
import ( "entgo.io/ent" "entgo.io/ent/dialect" "entgo.io/ent/schema/field")
// User holds the schema definition for the User entity.type User struct { ent.Schema}
// Fields of the User.func (User) Fields() []ent.Field {
return []ent.Field{
field.Int32("id").SchemaType(map[string]string{ dialect.MySQL: "int(10)UNSIGNED", // Override MySQL. }).NonNegative().Unique(),
field.String("email").SchemaType(map[string]string{ dialect.MySQL: "varchar(50)", // Override MySQL. }),
field.String("type").SchemaType(map[string]string{ dialect.MySQL: "varchar(20)", // Override MySQL. }),
field.Time("created_at").SchemaType(map[string]string{ dialect.MySQL: "timestamp", // Override MySQL. }).Optional(),
field.Time("updated_at").SchemaType(map[string]string{ dialect.MySQL: "timestamp", // Override MySQL. }).Optional(), }
}
// Edges of the User.func (User) Edges() []ent.Edge { return nil}
复制代码

Mixin 复用字段

在实际应用中,我们经常需要会有一些通用的字段,比如:idcreated_atupdated_at等等。


那么,我们就一直的复制粘贴?这显然很是不优雅。


entgo 能够让我们复用这些字段吗?


答案显然是,没问题。


Mixin,就是办这个事儿的。


好,我们现在需要复用时间相关的字段:created_atupdated_at,那么我们可以:


package mixin
import ( "time"
"entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin")
type TimeMixin struct { mixin.Schema}
func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time("created_at"). Immutable(). Default(time.Now), field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now), field.Bool("deleted").Default(false), }}
复制代码


然后,我们就可以在 Schema 当中应用了,比如User,我们为它添加一个Mixin方法:


func (User) Mixin() []ent.Mixin {  return []ent.Mixin{    mixin.TimeMixin{},  }}
复制代码


生成代码再看,就有这 3 个字段了。

生成代码

有了以上的 Schema,我们就可以生成代码了。生成代码只能够官方提供的 CLI 工具ent来生成。


而使用 CLI 有两种途径可以走:直接使用命令行执行命令,还有一种就是利用了 go 的go:generate特性。

命令行直接执行命令生成

我们可以命令行进入ent文件夹,然后执行以下命令:


ent generate ./schema
复制代码

通过 generate.go 生成

直接运行命令看起来是没有问题,但是在我们实际应用当中,直接使用命令行的方式进行代码生成是很不方便的。


为什么呢?ent命令是有参数的,而在正常情况下,都是需要携带一些参数的:比如:--feature sql/modifier,具体文档在:特性开关


这时候,我们必须在某一个地方记录这些命令,而后续会有同事需要接手这个项目呢?他又从何而知?在这个时候就徒增了不少麻烦。


好在 go 有一个很赞的特性go:generate,可以完美的解决这样一个问题。命令可以以代码的形式被记录下来,方便的重复使用。


通常我们都会把 ent 相关的代码放置在ent文件夹下面,因此我们在ent文件夹下面创建一个generate.go文件:


package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature privacy --feature sql/modifier --feature entql --feature sql/upsert ./schema
复制代码


接着,我们可以在项目的根目录或者ent文件夹下,执行以下命令:


go generate ./...
复制代码


以上的命令会遍历执行当前以及所有子目录下面的go:generate


如果您使用的是 Goland 或者 VSC,则可以在 IDE 中直接运行go:generate命令。

ent 的一些数据库基本操作

因为数据库是复杂的,SQL 是复杂的,复杂到能够出好几本书,所以是绝不可能在简单的篇幅里面讲完整,只能够将常用的一些操作(连接数据库、CURD)拿来举例讲讲。

连接数据库

SQLite3

import (  _ "github.com/mattn/go-sqlite3")
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")if err != nil { log.Fatalf("failed opening connection to sqlite: %v", err)}defer client.Close()
复制代码

MySQL/MariaDB

  • TiDB 高度兼容 MySQL 5.7 协议

  • ClickHouse 支持 MySQL wire 通讯协议


import (  _ "github.com/go-sql-driver/mysql")
client, err := ent.Open("mysql", "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True")if err != nil { log.Fatalf("failed opening connection to mysql: %v", err)}defer client.Close()
复制代码

PostgreSQL

  • CockroachDB 兼容 PostgreSQL 协议


import (  _ "github.com/lib/pq")
client, err := ent.Open("postgresql", "host=<host> port=<port> user=<user> dbname=<database> password=<pass>")if err != nil { log.Fatalf("failed opening connection to postgres: %v", err)}defer client.Close()
复制代码

Gremlin

import (  "<project>/ent")
client, err := ent.Open("gremlin", "http://localhost:8182")if err != nil { log.Fatalf("failed opening connection to gremlin: %v", err)}defer client.Close()
复制代码

自定义驱动 sql.DB

有以下两种途径可以达成:


package main
import ( "time"
"<your_project>/ent" "entgo.io/ent/dialect/sql")
func Open() (*ent.Client, error) { drv, err := sql.Open("mysql", "<mysql-dsn>") if err != nil { return nil, err } // Get the underlying sql.DB object of the driver. db := drv.DB() db.SetMaxIdleConns(10) db.SetMaxOpenConns(100) db.SetConnMaxLifetime(time.Hour) return ent.NewClient(ent.Driver(drv)), nil}
复制代码


第二种是:


package main
import ( "database/sql" "time"
"<your_project>/ent" entsql "entgo.io/ent/dialect/sql")
func Open() (*ent.Client, error) { db, err := sql.Open("mysql", "<mysql-dsn>") if err != nil { return nil, err } db.SetMaxIdleConns(10) db.SetMaxOpenConns(100) db.SetConnMaxLifetime(time.Hour) // Create an ent.Driver from `db`. drv := entsql.OpenDB("mysql", db) return ent.NewClient(ent.Driver(drv)), nil}
复制代码

自动迁移 Automatic Migration

if err := client.Schema.Create(context.Background(), migrate.WithForeignKeys(false)); err != nil {  l.Fatalf("failed creating schema resources: %v", err)}
复制代码

增 Create

pedro := client.Pet.    Create().    SetName("pedro").    SaveX(ctx)
复制代码

删 Delete

err := client.User.    DeleteOneID(id).    Exec(ctx)
复制代码

改 Update

pedro, err := client.Pet.    UpdateOneID(id).    SetName("pedro").    SetOwnerID(owner).    Save(ctx)
复制代码

查 Read

names, err := client.Pet.    Query().    Select(pet.FieldName).    Strings(ctx)
复制代码

事务 Transaction

事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。


封装一个方法WithTx,利用匿名函数来调用被事务管理的 Insert、Update、Delete 语句:


package data
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error { tx, err := client.Tx(ctx) if err != nil { return err } defer func() { if v := recover(); v != nil { tx.Rollback() panic(v) } }() if err := fn(tx); err != nil { if rerr := tx.Rollback(); rerr != nil { err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr) } return err } if err := tx.Commit(); err != nil { return fmt.Errorf("committing transaction: %w", err) } return nil}
复制代码


使用方法:


func createUser(tx *ent.Tx, u UserData) *ent.UserCreate {  return tx.User.Create().    SetName(u.Name).    SetNillableNickName(u.NickName)}
func updateUser(tx *ent.Tx, u UserData) *ent.UserUpdate { return tx.User.Update(). Where( user.Name(u.Name), ). SetNillableNickName(u.NickName)}
func deleteUser(tx *ent.Tx, u UserData) *ent.UserDelete { return tx.User.Delete(). Where( user.Name(u.Name), )}
func batchCreateUser(ctx context.Context, tx *ent.Tx, users []UserData) error { userCreates := make([]*ent.UserCreate, 0) for _, u := range users { userCreates = append(userCreates, createUser(tx, u)) } if _, err := tx.User.CreateBulk(userCreates...).Save(ctx); err != nil { return err } return nil}
func DoBatchCreateUser(ctx context.Context, client *ent.Client) { if err := WithTx(ctx, client, func(tx *ent.Tx) error { return batchCreateUser(ctx, tx, users) }); err != nil { log.Fatal(err) }}
复制代码

创建 gRPC 接口

如果你已经有了数据库的表结构,当你开始初始化一个项目的时候,你不必写任何一行代码,就完成了从 ent 的数据库定义,到网络 API 定义的全流程。接着,你需要做的,也就是微调,然后开始撸业务逻辑代码了。不要太开心!现在不都流行所谓的“低代码”吗?这不就是吗!


安装 protoc 插件:


go install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install entgo.io/contrib/entproto/cmd/protoc-gen-entgrpc@latest
复制代码


向项目添加依赖库:


go get -u entgo.io/contrib/entproto
复制代码


Schema添加entproto.Message()entproto.Service()方法:


func (User) Annotations() []schema.Annotation {    return []schema.Annotation{        entproto.Message(),        entproto.Service(      entproto.Methods(entproto.MethodCreate | entproto.MethodGet | entproto.MethodList | entproto.MethodBatchCreate),    ),    }}
复制代码


其中,entproto.Message()将会导致生成 Protobuf 的messageentproto.Service()将会导致生成 gRPC 的service


使用entproto.Field()方法向表字段添加 Protobuf 的字段索引号:


func (User) Fields() []ent.Field {    return []ent.Field{        field.String("name").            Unique().            Annotations(                entproto.Field(2),            ),        field.String("email_address").            Unique().            Annotations(                entproto.Field(3),            ),    }}
复制代码


generate.go添加entgo.io/contrib/entproto/cmd/entproto命令:


package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema//go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema
复制代码


执行生成命令:


go generate ./...
复制代码


将会生成以下文件:


ent/proto/entpb├── entpb.pb.go├── entpb.proto├── entpb_grpc.pb.go├── entpb_user_service.go└── generate.go
复制代码


生成的entpb.proto文件生成的内容可能会是这样的:


// Code generated by entproto. DO NOT EDIT.syntax = "proto3";
package entpb;
option go_package = "ent-grpc-example/ent/proto/entpb";
message User { int32 id = 1;
string user_name = 2;
string email_address = 3;}
service UserService { rpc Create ( CreateUserRequest ) returns ( User );
rpc Get ( GetUserRequest ) returns ( User );
rpc Update ( UpdateUserRequest ) returns ( User );
rpc Delete ( DeleteUserRequest ) returns ( google.protobuf.Empty );
rpc List ( ListUserRequest ) returns ( ListUserResponse );
rpc BatchCreate ( BatchCreateUsersRequest ) returns ( BatchCreateUsersResponse );}
复制代码

与 Kratos 携起手来

官方推荐的包结构是这样的:


|- data  |- biz  |- service  |- server  
复制代码


那么,我们可以把ent放进data文件夹去:


|- data  |  |- ent  |- biz  |- service  |- server
复制代码


需要说明的是,项目的结构、命名的规范这些并不在本文阐述的范围之内。并非说非要如此,这个可以根据各自的情况来灵活设计。


我使用这样的项目结构和命名规范,仅仅是为了方便讲清楚如何在 Kratos 中去引用 Ent。

创建数据库客户端

data/data.go文件中添加创建 Ent 数据库客户端的方法NewEntClient


package data
// ProviderSet is data providers.var ProviderSet = wire.NewSet( NewEntClient, ...)
// Data .type Data struct { db *ent.Client}
// NewEntClient 创建数据库客户端func NewEntClient(conf *conf.Data, logger log.Logger) *ent.Client { l := log.NewHelper(log.With(logger, "module", "ent/data"))
client, err := ent.Open( conf.Database.Driver, conf.Database.Source, ) if err != nil { l.Fatalf("failed opening connection to db: %v", err) } // 运行数据库迁移工具 if conf.Database.Migrate { if err := client.Schema.Create(context.Background(), migrate.WithForeignKeys(false)); err != nil { l.Fatalf("failed creating schema resources: %v", err) } } return client}
复制代码


并将之注入到ProviderSet


// ProviderSet is data providers.var ProviderSet = wire.NewSet(    NewEntClient,    ...)
复制代码


需要说明的是数据库迁移工具,如果数据库中不存在表,迁移工具会创建一个;如果字段存在改变,迁移工具会对字段进行修改。

创建 UseCase

在 biz 文件夹下创建user.go


package biz
type UserRepo interface { ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (bool, error)}
type UserUseCase struct { repo UserRepo log *log.Helper}
func NewUserUseCase(repo UserRepo, logger log.Logger) *UserUseCase { l := log.NewHelper(log.With(logger, "module", "user/usecase")) return &UserUseCase{repo: repo, log: l}}
func (uc *UserUseCase) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) { return uc.repo.ListUser(ctx, req)}
func (uc *UserUseCase) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) { return uc.repo.GetUser(ctx, req)}
func (uc *UserUseCase) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) { return uc.repo.CreateUser(ctx, req)}
func (uc *UserUseCase) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) { return uc.repo.UpdateUser(ctx, req)}
func (uc *UserUseCase) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) { return uc.repo.DeleteUser(ctx, req)}
复制代码


注入到biz.ProviderSet


package biz
// ProviderSet is biz providers.var ProviderSet = wire.NewSet( NewUserUseCase, ...)
复制代码

创建 Repo

data文件夹下创建user.go文件,实际操作数据库的操作都在此处。


package data
var _ biz.UserRepo = (*UserRepo)(nil)
type UserRepo struct { data *Data log *log.Helper}
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo { l := log.NewHelper(log.With(logger, "module", "User/repo")) return &UserRepo{ data: data, log: l, }}
func (r *UserRepo) convertEntToProto(in *ent.User) *v1.User { if in == nil { return nil } return &v1.User{ Id: in.ID, UserName: in.UserName, NickName: in.NickName, Password: in.Password, CreateTime: util.UnixMilliToStringPtr(in.CreateTime), UpdateTime: util.UnixMilliToStringPtr(in.UpdateTime), }}
func (r *UserRepo) Count(ctx context.Context, whereCond entgo.WhereConditions) (int, error) { builder := r.data.db.User.Query() if len(whereCond) != 0 { for _, cond := range whereCond { builder = builder.Where(cond) } } return builder.Count(ctx)}
func (r *UserRepo) List(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) { whereCond, orderCond := entgo.QueryCommandToSelector(req.GetQuery(), req.GetOrderBy())
builder := r.data.db.User.Query()
if len(whereCond) != 0 { for _, cond := range whereCond { builder = builder.Where(cond) } } if len(orderCond) != 0 { for _, cond := range orderCond { builder = builder.Order(cond) } } else { builder.Order(ent.Desc(user.FieldCreateTime)) } if req.GetPage() > 0 && req.GetPageSize() > 0 && !req.GetNopaging() { builder. Offset(paging.GetPageOffset(req.GetPage(), req.GetPageSize())). Limit(int(req.GetPageSize())) } results, err := builder.All(ctx) if err != nil { return nil, err }
items := make([]*v1.User, 0, len(results)) for _, res := range results { item := r.convertEntToProto(res) items = append(items, item) }
count, err := r.Count(ctx, whereCond) if err != nil { return nil, err }
return &v1.ListUserResponse{ Total: int32(count), Items: items, }, nil}
func (r *UserRepo) Get(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) { res, err := r.data.db.User.Get(ctx, req.GetId()) if err != nil && !ent.IsNotFound(err) { return nil, err }
return r.convertEntToProto(res), err}
func (r *UserRepo) Create(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) { cryptoPassword, err := crypto.HashPassword(req.User.GetPassword()) if err != nil { return nil, err }
res, err := r.data.db.User.Create(). SetNillableUserName(req.User.UserName). SetNillableNickName(req.User.NickName). SetPassword(cryptoPassword). SetCreateTime(time.Now().UnixMilli()). Save(ctx) if err != nil { return nil, err }
return r.convertEntToProto(res), err}
func (r *UserRepo) Update(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) { cryptoPassword, err := crypto.HashPassword(req.User.GetPassword()) if err != nil { return nil, err }
builder := r.data.db.User.UpdateOneID(req.Id). SetNillableNickName(req.User.NickName). SetPassword(cryptoPassword). SetUpdateTime(time.Now().UnixMilli())
res, err := builder.Save(ctx) if err != nil { return nil, err }
return r.convertEntToProto(res), err}
func (r *UserRepo) Delete(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) { err := r.data.db.User. DeleteOneID(req.GetId()). Exec(ctx) return err != nil, err}
复制代码


注入到data.ProviderSet


package data
// ProviderSet is data providers.var ProviderSet = wire.NewSet( NewUserRepo, ...)
复制代码

在 Service 中调用

package service
type UserService struct { v1.UnimplementedUserServiceServer
uc *biz.UserUseCase log *log.Helper}
func NewUserService(logger log.Logger, uc *biz.UserUseCase) *UserService { l := log.NewHelper(log.With(logger, "module", "service/user")) return &UserService{ log: l, uc: uc, }}
// ListUser 获取用户列表func (s *UserService) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) { return s.uc.ListUser(ctx, req)}
// GetUser 获取一个用户func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) { return s.uc.GetUser(ctx, req)}
// CreateUser 创建一个用户func (s *UserService) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) { return s.uc.CreateUser(ctx, req)}
// UpdateUser 更新一个用户func (s *UserService) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) { return s.uc.UpdateUser(ctx, req)}
// DeleteUser 删除一个用户func (s *UserService) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (*emptypb.Empty, error) { _, err := s.uc.DeleteUser(ctx, req) if err != nil { return nil, err } return &emptypb.Empty{}, nil}
复制代码


注入到service.ProviderSet


package service
// ProviderSet is data providers.var ProviderSet = wire.NewSet( NewUserService, ...)
复制代码


将服务注册到 gRPC 服务器当中去:


package server
// NewGRPCServer new a gRPC server.func NewGRPCServer(cfg *conf.Bootstrap, logger log.Logger, userSvc *service.UserService,) *grpc.Server { srv := bootstrap.CreateGrpcServer(cfg, logging.Server(logger))
userV1.RegisterUserServiceServer(srv, userSvc)
return srv}
复制代码


这样,我们就有了一个完整的用户服务

实例代码

结语

Ent 是一个优秀的 ORM 框架。基于模板进行代码生成,相比较利用反射等方式,在性能上的损耗更少。并且,模板的使用使得扩展系统变得简单容易。


它不仅能够很对传统的关系数据库(MySQL、PostgreSQL、SQLite)方便的进行查询,并且可以容易的进行图遍历——常用的譬如像是:菜单树、组织树……这种数据查询。


Ent 的工具链完整。对 gRPC 和 GraphQL 也支持的极好,也有相应的一系列工具链进行支持。从数据库表可以用工具转换成 Ent 的 Schema,从 Schema 可以生成 gRPC 和 GraphQL 的 API 的接口。Kratos 的 RPC 就是基于的 gRPC,也支持 GraphQL,简直就是为 Kratos 量身定做的。


相比较其他的 ORM 框架,Ent 对工程化的支持是极佳的,这对于开发维护的效率将会有极大的提升,几个项目下来,受益良多。个人而言,我是极力推崇的。

参考资料

  1. 官方网站: https://entgo.io/

  2. 官方文档: https://entgo.io/zh/docs/getting-started/

  3. 代码仓库: https://github.com/ent/ent

  4. SQL 转 Schema 在线工具: https://printlove.cn/tools/sql2ent

  5. ORM 实例教程 - 阮一峰: http://www.ruanyifeng.com/blog/2019/02/orm-tutorial.html

用户头像

喵个咪

关注

还未添加个人签名 2022-06-01 加入

还未添加个人简介

评论

发布
暂无评论
Golang微服框架Kratos与它的小伙伴系列 - ORM框架 - Ent_golang_喵个咪_InfoQ写作社区