跳到主要内容

· 阅读需 4 分钟

迁移到新的 ORM 并不容易,且过渡成本对许多组织来说可能是难以承受的。尽管我们开发者对「Shiny New Things」充满热情,现实中我们很少有机会参与真正的「绿地」(green‑field)项目。大多数职业生涯中,我们在受限于诸多技术与商业约束(即遗留系统)的情境下工作,限制了我们前进的选项。想要成功的技术创新者,需要提供互操作能力与集成路径,帮助组织无缝迁移到解决现有问题的新方式。

为了降低迁移到 Ent(或仅仅是尝试 Ent)的成本,我们启动了 "Schema Import Initiative",来支持从外部资源生成 Ent 模式的多种用例。该项目的核心是 schemast 包(源代码文档),它使开发者能够轻松编写程序来生成和操作 Ent 模式。使用此包,开发者可以在高层 API 上编程,免去对代码解析和 AST 操作的担忧。

Protobuf Import 支持

第一个使用此新 API 的项目是 protoc-gen-ent,一个 protoc 插件,用来从 .proto 文件生成 Ent 模式(文档)。已有在 Protobuf 中定义模式的组织可以利用此工具自动生成 Ent 代码。例如,给出一个简单的消息定义:

syntax = "proto3";

package entpb;

option go_package = "github.com/yourorg/project/ent/proto/entpb";

message User {
string name = 1;
string email_address = 2;
}

并将 ent.schema.gen 选项设置为 true:

syntax = "proto3";

package entpb;

+import "options/opts.proto";

option go_package = "github.com/yourorg/project/ent/proto/entpb";

message User {
+ option (ent.schema).gen = true; // <-- tell protoc-gen-ent you want to generate a schema from this message
string name = 1;
string email_address = 2;
}

开发者可以调用标准 protoc(protobuf 编译器)命令来使用此插件:

protoc -I=proto/ --ent_out=. --ent_opt=schemadir=./schema proto/entpb/user.proto

以从这些定义生成 Ent 模式:

package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{field.String("name"), field.String("email_address")}
}
func (User) Edges() []ent.Edge {
return nil
}

想要今天开始使用 protoc-gen-ent,并了解所有不同配置选项,请前往文档

加入 Schema Import Initiative

你是否有定义在其他地方的模式,希望能自动导入到 Ent?使用 schemast 包,编写你所需工具从未如此简单。不知道从何开始?想与社区共同规划和构建你的想法?欢迎通过我们的 Discord 服务器(Discord 服务器)、Slack 频道(Slack 频道)或在 GitHub 讨论(GitHub 讨论)中与我们联系!

更多 Ent 新闻与更新

· 阅读需 15 分钟

ent + gRPC

简介

将实体模式定义在一个中心、语言无关的格式中,随着软件工程组织规模的增长,这样做有许多好处[链接]。为此,许多组织使用 Protocol Buffers 作为它们的接口定义语言 (IDL)。另外,gRPC 是一个基于 Protobuf 的 RPC 框架,模仿了 Google 内部的 Stubby,由于其效率和代码生成能力而变得越来越流行。

作为 IDL,gRPC 并不规定实现数据访问层的具体准则,因此实现方式差异很大。Ent 是在任何 Go 应用中构建数据访问层的天然候选者,二者集成具有很大潜力。

今天我们宣布 entproto 的实验版本,一个 Go 包和命令行工具,为 Ent 用户添加 Protobuf 与 gRPC 支持。使用 entproto,开发者可以在几分钟内搭建一个完全可运行的 CRUD gRPC 服务器。本文将详细展示实现步骤。

设置项目

最终的完整教程可在 GitHub 查看,你也可以直接克隆它。

首先为项目初始化一个新的 Go 模块:

mkdir ent-grpc-example
cd ent-grpc-example
go mod init ent-grpc-example

接下来使用 go run 调用 Ent 代码生成器,初始化一个 schema:

go run -mod=mod entgo.io/ent/cmd/ent new User

此时目录结构应为:

.
├── ent
│   ├── generate.go
│   └── schema
│   └── user.go
├── go.mod
└── go.sum

然后将 entproto 包加入项目:

go get -u entgo.io/contrib/entproto

接下来,我们为 User 实体定义 schema。打开 ent/schema/user.go 并编辑:

package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema"
"entgo.io/ent/schema/field"
)

// User 持有 User 实体的模式定义。
type User struct {
ent.Schema
}

// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique(),
field.String("email_address").
Unique(),
}
}

在此步骤中,我们为 User 实体添加了两个唯一字段:nameemail_addressent.Schema 仅是模式定义;若要生成可用的生产代码,需要对其运行 Ent 的代码生成工具:

go generate ./...

生成后将出现一大堆新的文件:

├── ent
│   ├── client.go
│   ├── config.go
│   └── ...
│   ├── user
│   ├── user.go
│   ├── user_create.go
│   ├── user_delete.go
│   ├── user_query.go
│   └── user_update.go
├── go.mod
└── go.sum

此时我们已经可以打开数据库连接、运行迁移创建 users 表,并对其进行读写。详细步骤请参阅Setup Tutorial。接下来让我们学习如何从模式生成 Protobuf 定义与 gRPC 服务器。

使用 entproto 生成 Go Protobuf

由于 Ent 与 Protobuf 的模式并不相同,我们需要给模式加上注解,帮助 entproto 确定如何生成 Protobuf 定义(在 Protobuf 术语中称为 Messages)。

第一步,我们要给 User 添加 entproto.Message() 注解。该注解是我们对 Protobuf 模式生成的选择性开启;我们并不一定想从所有模式实体生成 proto 消息或 gRPC 服务定义,注解给予我们该控制。为此,在 ent/schema/user.go 中添加:

func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
}
}

下一步,需要为每个字段加上 entproto.Field 注解并指定字段编号。回忆一下,在定义 protobuf 消息类型时,每个字段都需被唯一编号。对此,在 ent/schema/user.goFields 中做如下更新:

// User 的字段。
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),
),
}
}

请注意,我们没有从 1 开始编号,因为 Ent 会隐式创建 ID 字段,并自动分配编号 1。接下来,我们可以生成 protobuf 消息类型定义。为此,我们将在 ent/generate.go 中添加 go:generate 指令,调用 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,其中包含所有 protobuf 相关生成代码:

ent/proto
└── entpb
├── entpb.proto
└── generate.go

两个文件已生成。先来看它们的内容:

// 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;
}

太好了!已经生成了一个 .proto 文件,其中包含与我们的 User 模式对应的消息类型定义。

package entpb
//go:generate protoc -I=.. --go_out=.. --go-grpc_out=.. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --entgrpc_out=.. --entgrpc_opt=paths=source_relative,schema_path=../../schema entpb/entpb.proto

generate.go 中的 protoc 调用会把 .proto 文件编译为 Go 代码。要使该命令正常工作,需先安装 protoc 以及 3 个 Protobuf 插件:protoc-gen-go(生成 Go Protobuf 结构体)、protoc-gen-go-grpc(生成 Go gRPC 服务接口和客户端),以及 protoc-gen-entgrpc(生成服务实现)。若尚未安装,请按如下步骤:

安装完这些依赖后,再次执行代码生成:

go generate ./...

这时会生成一个新文件 ent/proto/entpb/entpb.pb.go,其中包含为我们实体生成的 Go 结构体。

接下来编写一个测试,使用它确保一切正确连接。新建文件 pb_test.go 并写入:

package main

import (
"testing"

"ent-grpc-example/ent/proto/entpb"
)

func TestUserProto(t *testing.T) {
user := entpb.User{
Name: "rotemtam",
EmailAddress: "rotemtam@example.com",
}
if user.GetName() != "rotemtam" {
t.Fatal("预期用户名称为 rotemtam")
}
if user.GetEmailAddress() != "rotemtam@example.com" {
t.Fatal("预期邮箱地址为 rotemtam@example.com")
}
}

运行测试:

go get -u ./... # 安装生成包的依赖
go test ./...

如预期,测试通过。我们已经成功从 Ent 模式生成了可用的 Go Protobuf 结构体。接下来,让我们看看如何自动生成一个完整的 CRUD gRPC 服务器

从模式生成完整可用的 gRPC 服务器

生成 Protobuf 结构体固然实用,但我们真正想要的是能够对实际数据库中的实体执行创建、读取、更新、删除的服务器。为此,只需更新一行代码!当我们给模式加上 entproto.Service 注解时,我们告诉 entproto 代码生成器我们想生成一个 gRPC 服务定义,随后 protoc-gen-entgrpc 将读取我们的定义并生成服务实现。编辑 ent/schema/user.go 并修改 Annotations

func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
+ entproto.Service(), // <-- 添加此行
}
}

重新运行代码生成:

go generate ./...

现在在 ent/proto/entpb 中会出现一些变化:

ent/proto/entpb
├── entpb.pb.go
├── entpb.proto
├── entpb_grpc.pb.go
├── entpb_user_service.go
└── generate.go

首先,entprotoentpb.proto 中添加了服务定义:

service UserService {
rpc Create ( CreateUserRequest ) returns ( User );

rpc Get ( GetUserRequest ) returns ( User );

rpc Update ( UpdateUserRequest ) returns ( User );

rpc Delete ( DeleteUserRequest ) returns ( google.protobuf.Empty );
}

另外,新增了两份文件。第一个 ent_grpc.pb.go 包含了 gRPC 客户端存根和接口定义。若打开文件,可见以下片段:

// UserServiceClient 是 UserService 服务的客户端 API。
//
// 关于 ctx 使用语义与关闭/结束流式 RPC,请参阅 https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream。
type UserServiceClient interface {
Create(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)
Get(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
Update(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
Delete(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}

第二份 entpub_user_service.go 则包含该接口的实现。例如,Get 方法的实现:

// Get 实现 UserServiceServer.Get
func (svc *UserService) Get(ctx context.Context, req *GetUserRequest) (*User, error) {
get, err := svc.client.User.Get(ctx, int(req.GetId()))
switch {
case err == nil:
return toProtoUser(get), nil
case ent.IsNotFound(err):
return nil, status.Errorf(codes.NotFound, "not found: %s", err)
default:
return nil, status.Errorf(codes.Internal, "internal error: %s", err)
}
}

不错!接下来让我们创建一个可以服务请求的 gRPC 服务器。

创建服务器

新建文件 cmd/server/main.go 并写入:

package main

import (
"context"
"log"
"net"

_ "github.com/mattn/go-sqlite3"
"ent-grpc-example/ent"
"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
)

// main 启动服务器。
func main() {
// 初始化 ent 客户端。
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("打开 sqlite 连接失败: %v", err)
}
defer client.Close()

// 运行迁移工具(创建表等)。
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("创建 schema 资源失败: %v", err)
}

// 初始化生成的 User 服务。
svc := entpb.NewUserService(client)

// 创建一个新的 gRPC 服务器(你可以在同一服务器上挂载多个服务)。
server := grpc.NewServer()

// 在服务器上注册 User 服务。
entpb.RegisterUserServiceServer(server, svc)

// 打开 5000 端口监听流量。
lis, err := net.Listen("tcp", ":5000")
if err != nil {
log.Fatalf("监听失败: %s", err)
}

// 持续监听流量。
if err := server.Serve(lis); err != nil {
log.Fatalf("服务器遇到错误: %s", err)
}
}

请注意我们已添加了 github.com/mattn/go-sqlite3 的引用,因此需要将其加入模块:

go get -u github.com/mattn/go-sqlite3

接下来,启动服务器,随后再写一个客户端与其通信:

go run -mod=mod ./cmd/server

创建客户端

让我们编写一个简单的客户端来调用我们的服务器。新建文件 cmd/client/main.go 并写入:

package main

import (
"context"
"fmt"
"log"
"math/rand"
"time"

"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

// main 启动客户端。
func main() {
rand.Seed(time.Now().UnixNano())

// 连接到服务器。
conn, err := grpc.Dial(":5000", grpc.WithInsecure())
if err != nil {
log.Fatalf("连接服务器失败: %s", err)
}
defer conn.Close()

// 在连接上创建 User 服务客户端。
client := entpb.NewUserServiceClient(conn)

// 请求服务器创建一个随机 User。
ctx := context.Background()
user := randomUser()
created, err := client.Create(ctx, &entpb.CreateUserRequest{
User: user,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("创建用户失败: status=%s message=%s", se.Code(), se.Message())
}
log.Printf("已创建用户,ID 为: %d", created.Id)

// 在单独一次 RPC 调用中,检索之前保存的用户。
get, err := client.Get(ctx, &entpb.GetUserRequest{
Id: created.Id,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("检索用户失败: status=%s message=%s", se.Code(), se.Message())
}
log.Printf("检索到用户,ID=%d: %v", get.Id, get)
}

// randomUser 生成一个随机 User。
func randomUser() *entpb.User {
return &entpb.User{
Name: fmt.Sprintf("user_%d", rand.Int()),
EmailAddress: fmt.Sprintf("user_%d@example.com", rand.Int()),
}
}

我们的客户端连接到 5000 端口(服务器正在监听),然后执行 Create 请求创建新用户,随后执行第二次 Get 请求检索刚存入数据库中的用户。运行客户端:

go run ./cmd/client

输出示例:

2021/03/18 10:42:58 已创建用户,ID 为: 1
2021/03/18 10:42:58 检索到用户,ID=1: id:1 name:"user_730811260095307266" email_address:"user_7338662242574055998@example.com"

太棒了!只需在模式上添加几个注解,即可利用强大的代码生成能力快速创建一个可用的 gRPC 服务器。

注意事项与限制

entproto 仍处于实验阶段,缺少一些基本功能。例如,许多应用可能希望在服务中加入 ListFind 方法,但目前尚不支持。未来还计划解决以下问题:

  • 目前仅支持“唯一”边(O2O、O2M)。
  • 生成的“变异”方法(Create/Update)目前会设置所有字段,忽略零值/空值和字段可空性。
  • 所有字段均会被复制自 gRPC 请求到 ent 客户端,计划提供通过字段/边注解将某些字段设置为不可在服务中修改的功能。

下一步

我们相信 ent + gRPC 可以成为在 Go 中构建服务器应用的绝佳方法。例如,想为应用管理的实体设置细粒度访问控制,开发者可以使用 Privacy Policies(隐私政策),它们已与 gRPC 集成原生支持。若要在实体的不同生命周期事件上运行任意 Go 代码,开发者可利用自定义 Hooks(钩子)。

你想用 ent 构建 gRPC 服务器吗?如果你需要帮助设置,或希望集成支持你的用例,请通过我们的 GitHub 讨论页面 与我们联系,或者加入 #ent 频道在 Gophers Slack 或我们的 Discord 服务器 进行交流。

更多 Ent 新闻与更新:
  • 订阅我们的简报
  • 关注我们的 Twitter
  • 在 Gophers Slack 的 #ent 频道加入我们
  • 加入我们的 Ent Discord 服务器

· 阅读需 6 分钟

在过去的几个月里,Ent 项目的 issues 中,关于在检索一对一或一对多边缘的实体时追加检索外键字段的讨论颇多。我们很高兴宣布,从 v0.7.0 开始,Ent 已支持此功能。

Edge-field 支持之前

在合并此分支之前,想要检索一个实体的外键字段的用户需要使用预加载。假设我们的架构如下所示:

// ent/schema/user.go:

// 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.String("name").
Unique().
NotEmpty(),
}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("pets", Pet.Type).
Ref("owner"),
}
}

// ent/schema/pet.go

// Pet holds the schema definition for the Pet entity.
type Pet struct {
ent.Schema
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Unique().
Required(),
}
}

该架构描述了两个关联实体:UserPet,它们之间存在一对多边缘:一个用户可以拥有多只宠物,而一只宠物只能有一个所有者。

当从存储库检索宠物时,开发者通常需要访问宠物上的外键字段。然而,由于该字段是从 owner 边缘隐式创建的,它在检索实体时自动可访问。若想从存储中检索此字段,开发者需要执行类似以下的代码:

func Test(t *testing.T) {
ctx := context.Background()
c := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
defer c.Close()

// Create the User
u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)

// Create the Pet
p := c.Pet.
Create().
SetOwner(u). // Associate with the user
SetName("donut").
SaveX(ctx)

petWithOwnerId := c.Pet.Query().
Where(pet.ID(p.ID)).
WithOwner(func(query *ent.UserQuery) {
query.Select(user.FieldID)
}).
OnlyX(ctx)
fmt.Println(petWithOwnerId.Edges.Owner.ID)
// Output: 1
}

除了过于冗长之外,以这种方式检索带拥有者的宠物在数据库查询方面也低效。如果我们使用 .Debug() 执行查询,可以看到 Ent 为满足此调用生成的 DB 查询:

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`pet_owner` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2 
SELECT DISTINCT `users`.`id` FROM `users` WHERE `users`.`id` IN (?)

在此示例中,Ent 首先检索 ID 为 1 的宠物,然后冗余地从 users 表中获取 ID 为 1 的用户的 id 字段。

使用 Edge-field 支持

Edge-field 支持极大简化并提升了此流程的效率。通过此功能,开发者可以在架构的 Fields() 中定义外键字段,并在边缘定义时使用 .Field(..) 修饰符告诉 Ent 映射该列到此字段。因此,在我们的示例架构中,我们可以按如下方式修改:

// user.go stays the same

// pet.go
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id"), // <-- explicitly add the field we want to contain the FK
}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Field("owner_id"). // <-- tell ent which field holds the reference to the owner
Unique().
Required(),
}
}

为了更新我们的客户端代码,需要重新运行代码生成:

go generate ./...

现在我们可以将查询改写得更简洁:

func Test(t *testing.T) {
ctx := context.Background()
c := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
defer c.Close()

u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)

p := c.Pet.Create().
SetOwner(u).
SetName("donut").
SaveX(ctx)

petWithOwnerId := c.Pet.GetX(ctx, p.ID) // <-- Simply retrieve the Pet

fmt.Println(petWithOwnerId.OwnerID)
// Output: 1
}

使用 .Debug() 修饰符运行时,可以看到 DB 查询现在更合理:

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`owner_id` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2

太好了 🎉!

将现有架构迁移至 Edge-fields

如果你已经在使用现有架构的 Ent,可能已经有 O2M 关系在数据库中存在外键列。根据你如何配置架构,它们可能存储在与现在添加的字段不同的列名中。例如,你想创建 owner_id 字段,但 Ent 自动创建的外键列为 pet_owner

要检查 Ent 为此字段使用的列名,你可以查看 ./ent/migrate/schema.go 文件:

PetsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString},
{Name: "pet_owner", Type: field.TypeInt, Nullable: true}, // <-- this is our FK
}

为了实现平滑迁移,你必须明确告诉 Ent 继续使用现有列名。你可以使用 StorageKey 修饰符(可以在字段或边缘上使用)来实现。例如:

// In schema/pet.go:

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id").
StorageKey("pet_owner"), // <-- explicitly set the column name
}
}

在不久的将来,我们计划实现架构版本化,它将把架构更改的历史与代码存储在一起。拥有这些信息将使 ent 能够以自动且可预测的方式支持此类迁移。

总结

Edge-field 支持已准备就绪,可通过 go get -u entgo.io/ent@v0.7.0 安装。

感谢所有花时间提供反馈并帮助我们妥善设计此功能的优秀人士: Alex SnastRuben de VriesMarwan SulaimanAndy DaySebastian FeketeJoe Harvey

更多 Ent 动态与更新:

· 阅读需 4 分钟

Facebook 连接中心以色列分部的 Go 状态

20 个月前,我加入了 Facebook Connectivity(FBC)团队,地点是以色列特拉维夫。此前我已经有大约 5 年的 Go 编程经验,并在几家公司将其嵌入到产品中。

我加入的团队正在进行一个新项目,必须为此任务选择一种语言。我们比较了几种语言,并决定采用 Go。

从那时起,Go 继续在其他 FBC 项目中扩散,并在特拉维夫仅有的 15 名 Go 工程师中取得了巨大成功。新的服务现在都是用 Go 编写的

编写新的 Go ORM 的动机

在 Facebook 加入之前,我 5 年的工作大多是基础设施工具和微服务开发,并没有太多的数据模型工作。需要与 SQL 数据库做少量工作的一项服务使用了现有的开源方案,但若需要处理更为复杂的数据模型,则使用了在另一种语言中实现的强大 ORM,例如 Python + SQLAlchemy。

在 Facebook,我们喜欢用图形概念来思考数据模型。我们在内部对此模型有过良好体验。缺乏一个合适的基于图的 Go ORM,促使我们在这里编写一个,遵循以下原则:

  • Schema As Code:在 Go 代码中(而非 struct tag)定义类型、关系和约束,并使用 CLI 工具进行验证。我们在 Facebook 内部已有类似工具的良好经验。
  • 静态类型且明确的 API:使用代码生成。全局使用 interface{} 的 API 会降低开发者效率,尤其是新人。
  • 查询、聚合和图遍历应当简单——开发者不想处理原始 SQL 语句或 SQL 术语。
  • 谓词应为静态类型:不要随处使用字符串。
  • 完全支持 context.Context——这有助于我们在跟踪和日志系统中获得完整可见性,并且对取消等功能至关重要。
  • 存储无关:我们尝试通过代码生成模板保持存储层动态,最初在 Gremlin(AWS Neptune)上开发,后续切换到 MySQL。

开源 ent

ent 是一个基于上述原则构建的 Go 实体框架(ORM)。它使得在 Go 代码中轻松定义任何数据模型或图结构成为可能;模式配置由 entc(ent 的代码生成器)验证,生成符合惯用法且静态类型化的 API,从而保持 Go 开发者的高效与满意。它支持 MySQL、MariaDB、PostgreSQL、SQLite 以及基于 Gremlin 的图数据库。

我们今天开源 ent,邀请你开始使用 → entgo.io/docs/getting-started