0%

ent 快速入门

这篇文章我们将快速学习 ent 库,ent 是一个简单而又功能强大的 Go 语言实体框架,易于构建和维护应用程序与大数据模型。我们将通过一个实际例子来入门 ent 的使用,核心材料来自于 ent 的官方文档

ent 简介

ent 是一个简单而强大的 Go 实体框架,可以轻松构建和维护具有大型数据模型的应用程序,并遵循以下原则:

  • 轻松地将数据库 Schema 建模为图结构。
  • 将 Schema 定义为可编程的 Go 代码。
  • 基于代码生成的静态类型。
  • 数据库查询和图遍历易于编写。
  • 使用 Go 模板轻松扩展和自定义。

ent 遵循以下设计理念:

  • 图就是代码 - 将任何数据库表建模为 Go 对象
  • 轻松地遍历任何图形 - 可以轻松地运行查询、聚合和遍历任何图形结构
  • 静态类型和显式API - 使用代码生成静态类型和显式API,查询数据更加便捷
  • 多存储驱动程序 - 支持MySQL, PostgreSQL, SQLite 和 Gremlin
  • 可扩展 - 简单地扩展和使用 Go 模板自定义

创建 Schema

首先我们准备好一个 Go 项目:

1
2
3
4
# mkdir entdemo
# cd entdemo
# go mod init entdemo
go: creating new go.mod: module entdemo

在项目的根目录,运行:

1
2
# go run -mod=mod entgo.io/ent/cmd/ent new User
go: finding module for package entgo.io/ent/cmd/ent

上面的命令将在 ent/schema/ 目录下生成 User 的 Schema:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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
}

接下来向 User Schema 添加 2 个字段:

1
2
3
4
5
6
7
8
9
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}

从项目根目录运行 go generate,如下:

1
# go generate ./ent

该命令将生成如下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ls -l ent/
total 124
-rw-r--r-- 1 root root 9797 Jun 12 11:43 client.go
-rw-r--r-- 1 root root 16021 Jun 12 11:43 ent.go
drwxr-xr-x 2 root root 4096 Jun 12 11:43 enttest
-rw-r--r-- 1 root root 82 Jun 12 11:37 generate.go
drwxr-xr-x 2 root root 4096 Jun 12 11:43 hook
drwxr-xr-x 2 root root 4096 Jun 12 11:43 migrate
-rw-r--r-- 1 root root 12281 Jun 12 11:43 mutation.go
drwxr-xr-x 2 root root 4096 Jun 12 11:43 predicate
drwxr-xr-x 2 root root 4096 Jun 12 11:43 runtime
-rw-r--r-- 1 root root 837 Jun 12 11:43 runtime.go
drwxr-xr-x 2 root root 4096 Jun 12 11:37 schema
-rw-r--r-- 1 root root 6232 Jun 12 11:43 tx.go
drwxr-xr-x 2 root root 4096 Jun 12 11:43 user
-rw-r--r-- 1 root root 5649 Jun 12 11:43 user_create.go
-rw-r--r-- 1 root root 2133 Jun 12 11:43 user_delete.go
-rw-r--r-- 1 root root 3294 Jun 12 11:43 user.go
-rw-r--r-- 1 root root 14277 Jun 12 11:43 user_query.go
-rw-r--r-- 1 root root 7766 Jun 12 11:43 user_update.go

创建你的第一个实体

首先,创建一个新的 Client 来运行 Schema 迁移并与你的实体交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// entdemo/start.go

package main

import (
"context"
"log"

"entdemo/ent"

_ "github.com/mattn/go-sqlite3"
)

func main() {
client, err := ent.Open("sqlite3", "file:ent.db?cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
// Run the auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

运行代码,即可完成 Schema 迁移。我们可以看到对应的数据库文件和表格:

1
2
3
4
5
6
# sqlite3 ent.db ".tables"
users

# sqlite3 ent.db ".schema"
CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `age` integer NOT NULL, `name` text NOT NULL DEFAULT ('unknown'));
CREATE TABLE sqlite_sequence(name,seq);

创建并查询

如下代码完成 User 的创建和查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Create().
SetAge(30).
SetName("a8m").
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", u)
return u, nil
}

func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.Name("a8m")).
// `Only` 在没有找到用户或返回多于1个用户时失败。
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}
1
2
2026/06/12 12:06:53 user was created:  User(id=1, age=30, name=a8m)
2026/06/12 12:06:53 user returned: User(id=1, age=30, name=a8m)

注意,后面多次运行该示例,为了避免重复数据导致运行异常,每次可以先清理已有的数据库文件

添加边(关系)

接下来我们想要在 Schema 中声明到另一个实体的边(关系)。让我们创建2个名为 CarGroup 的额外实体,包含一些字段。我们使用 ent CLI 来生成初始 Schema:

1
go run -mod=mod entgo.io/ent/cmd/ent new Car Group

然后我们手动添加其余的字段:

entdemo/ent/schema/car.go

1
2
3
4
5
6
7
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}

entdemo/ent/schema/group.go

1
2
3
4
5
6
7
8
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// 对组名进行正则表达式验证。
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}

让我们定义我们的第一个关系:从 UserCar 的边,定义一个用户可以拥有1辆或多辆汽车,但一辆汽车只有一个所有者(一对多关系)。

  • 让我们向 User Schema 添加 "cars" 边,并运行 go generate ./ent

entdemo/ent/schema/user.go

1
2
3
4
5
6
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}

我们继续示例,创建2辆汽车并将它们添加到用户。

entdemo/start.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
// 创建一辆型号为 "Tesla" 的新车。
tesla, err := client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", tesla)

// 创建一辆型号为 "Ford" 的新车。
ford, err := client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", ford)

// 创建一个新用户,并将2辆汽车添加给它。
a8m, err := client.User.
Create().
SetAge(30).
SetName("a8m").
AddCars(tesla, ford).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", a8m)
return a8m, nil
}

那么通过 cars 边(关系)进行查询呢?以下是方法:

entdemo/start.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func QueryCars(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println("returned cars:", cars)
// 过滤特定的汽车。
ford, err := a8m.QueryCars().
Where(car.Model("Ford")).
Only(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println(ford)
return nil
}

main 函数中使用如下代码:

1
2
3
4
5
6
7
8
9
u, err := CreateCars(context.Background(), client)
if err != nil {
log.Fatalf("failed create cars: %v", err)
}

err = QueryCars(context.Background(), u)
if err != nil {
log.Fatalf("failed querying cars: %v", err)
}

运行结果如下:

1
2
3
4
5
6
7
# go run start.go

2026/06/12 12:22:08 car was created: Car(id=1, model=Tesla, registered_at=Fri Jun 12 12:22:08 2026)
2026/06/12 12:22:08 car was created: Car(id=2, model=Ford, registered_at=Fri Jun 12 12:22:08 2026)
2026/06/12 12:22:08 user was created: User(id=1, age=30, name=a8m)
2026/06/12 12:22:08 returned cars: [Car(id=1, model=Tesla, registered_at=Fri Jun 12 12:22:08 2026) Car(id=2, model=Ford, registered_at=Fri Jun 12 12:22:08 2026)]
2026/06/12 12:22:08 Car(id=2, model=Ford, registered_at=Fri Jun 12 12:22:08 2026)

此时查看数据库表的结构:

1
2
3
4
5
6
# sqlite3 ent.db ".schema
> "
CREATE TABLE `cars` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `model` text NOT NULL, `registered_at` datetime NOT NULL, `user_cars` integer NULL, CONSTRAINT `cars_users_cars` FOREIGN KEY (`user_cars`) REFERENCES `users` (`id`) ON DELETE SET NULL);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);
CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `age` integer NOT NULL, `name` text NOT NULL DEFAULT ('unknown'));
  • cars 表有一个 user_cars 字段,这是一个外键,指向 users 表

添加反向边

假设我们有一个 Car 对象,我们想要获取它的所有者,即这辆汽车所属的用户。为此,我们有另一种类型的边叫做 反向边,使用 edge.From 函数定义。新创建的边在图中是半透明的,以强调我们不会在数据库中创建另一条边。它只是对实际边(关系)的反向引用。

让我们向 Car Schema 添加一个名为 owner 的反向边,引用它到 User Schema 中的 cars 边,并运行 go generate ./ent

1
2
3
4
5
6
7
8
9
10
11
// Edges of the Car.
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// 创建一个名为 "owner" 的反向边,类型为 `User`
// 并使用 `Ref` 方法显式引用它到 "cars" 边(在 User Schema 中)。
edge.From("owner", User.Type).
Ref("cars").
// 将边设置为唯一,确保一辆汽车只能有一个所有者。
Unique(),
}
}

在主程序中查询反向边:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// entdemo/start.go

func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
// 查询反向边。
for _, c := range cars {
owner, err := c.QueryOwner().Only(ctx)
if err != nil {
return fmt.Errorf("failed querying car %q owner: %w", c.Model, err)
}
log.Printf("car %q owner: %q\n", c.Model, owner.Name)
}
return nil
}

运行结果如下:

1
2
3
4
# go run start.go
......
2026/06/15 18:38:24 car "Tesla" owner: "a8m"
2026/06/15 18:38:24 car "Ford" owner: "a8m"

数据库表结构和前面的例子一致:

1
2
3
4
5
# sqlite3 ent.db ".schema"
CREATE TABLE `cars` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `model` text NOT NULL, `registered_at` datetime NOT NULL, `user_cars` integer NULL, CONSTRAINT `cars_users_cars` FOREIGN KEY (`user_cars`) REFERENCES `users` (`id`) ON DELETE SET NULL);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);
CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `age` integer NOT NULL, `name` text NOT NULL DEFAULT ('unknown'));

创建多对多关系

我们继续示例,创建用户和组之间的 M2M(多对多)关系。如图所示,每个组实体可以拥有多个用户,一个用户可以连接到多个组;一个简单的 多对多 关系:

在上图中,Group Schema 是 users 边(关系)的所有者,User 实体有一个名为 groups 的反向边/反向引用到这个关系。让我们在 Schema 中定义这个关系:

1
2
3
4
5
6
7
8
// entdemo/ent/schema/group.go

// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// entdemo/ent/schema/user.go

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
// 创建一个名为 "groups" 的反向边,类型为 `Group`
// 并使用 `Ref` 方法显式引用它到 "users" 边(在 Group Schema 中)。
edge.From("groups", Group.Type).
Ref("users"),
}
}

我们在 Schema 目录运行 ent 以重新生成资源。

1
go generate ./ent

图遍历

运行我们的第一个图遍历,我们需要生成一些数据(节点和边,或者换句话说,实体和关系)。让我们使用框架创建以下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func CreateGraph(ctx context.Context, client *ent.Client) error {
// 首先,创建用户。
a8m, err := client.User.
Create().
SetAge(30).
SetName("Ariel").
Save(ctx)
if err != nil {
return err
}
neta, err := client.User.
Create().
SetAge(28).
SetName("Neta").
Save(ctx)
if err != nil {
return err
}
// 然后,创建汽车,并将它们附加到上面创建的用户。
err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()).
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
SetOwner(neta).
Exec(ctx)
if err != nil {
return err
}
// 创建组,并在创建时添加其用户。
err = client.Group.
Create().
SetName("GitLab").
AddUsers(neta, a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Group.
Create().
SetName("GitHub").
AddUsers(a8m).
Exec(ctx)
if err != nil {
return err
}
log.Println("The graph was created successfully")
return nil
}

现在我们有了包含数据的图,我们可以在上面运行几个查询:

  1. 获取组名为 “GitHub” 的所有用户汽车:
1
2
3
4
5
6
7
8
9
10
11
12
13
func QueryGithub(ctx context.Context, client *ent.Client) error {
cars, err := client.Group.
Query().
Where(group.Name("GitHub")). // (Group(Name=GitHub),)
QueryUsers(). // (User(Name=Ariel, Age=30),)
QueryCars(). // (Car(Model=Tesla), Car(Model=Mazda),)
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
return nil
}
  1. 更改上面的查询,使遍历的源是用户 Ariel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func QueryArielCars(ctx context.Context, client *ent.Client) error {
// 从前面的步骤获取 "Ariel"。
a8m := client.User.
Query().
Where(
user.HasCars(),
user.Name("Ariel"),
).
OnlyX(ctx)
cars, err := a8m.
QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
QueryCars().
Where(
car.Not(
car.Model("Mazda"), // 获取 Neta 和 Ariel 的汽车,但过滤掉名为 "Mazda" 的汽车
),
).
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
// 输出: (Car(Model=Tesla), Car(Model=Ford),)
return nil
}
  1. 获取所有有用户的组(使用附带谓词查询):
1
2
3
4
5
6
7
8
9
10
11
12
func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
groups, err := client.Group.
Query().
Where(group.HasUsers()).
All(ctx)
if err != nil {
return fmt.Errorf("failed getting groups: %w", err)
}
log.Println("groups returned:", groups)
// 输出: (Group(Name=GitHub), Group(Name=GitLab),)
return nil
}

运行结果:

1
2
3
4
2026/06/16 18:27:07 The graph was created successfully
2026/06/16 18:27:07 cars returned: [Car(id=3, model=Tesla, registered_at=Tue Jun 16 18:27:07 2026) Car(id=4, model=Mazda, registered_at=Tue Jun 16 18:27:07 2026)]
2026/06/16 18:27:07 cars returned: [Car(id=3, model=Tesla, registered_at=Tue Jun 16 18:27:07 2026) Car(id=5, model=Ford, registered_at=Tue Jun 16 18:27:07 2026)]
2026/06/16 18:27:07 groups returned: [Group(id=1, name=GitLab) Group(id=2, name=GitHub)]

Schema 迁移

Ent 提供两种运行 Schema 迁移的方法:自动迁移版本化迁移。以下是每种方法的简要概述:

自动迁移

使用自动迁移,用户可以使用以下 API 保持数据库 Schema 与在生成的 SQL Schema ent/migrate/schema.go 中定义的 Schema 对象对齐:

1
2
3
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}

这种方法主要用于原型设计、开发或测试。因此,对于关键的生产环境,建议使用版本化迁移方法。通过使用版本化迁移,用户可以事先知道将对数据库应用哪些更改,并根据需要轻松调整它们。

版本化迁移

自动迁移不同,版本化迁移方法使用 Atlas 自动生成一组包含必要 SQL 语句的迁移文件,以迁移数据库。这些文件可以编辑以满足特定需求,并使用现有的迁移工具如 Atlas、golang-migrate、Flyway 和 Liquibase 应用。此方法的 API 涉及两个主要步骤。

生成迁移

1
2
3
4
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1"

应用迁移

1
2
3
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "sqlite://file.db?_fk=1"

小结

这篇文章我们快速学习了 ent 库的基本使用方法,介绍了如何使用 ent 构建数据库模型,如何创建和查询数据。我们还介绍了如何在生产环境中使用 ent 迁移数据库 Schema 的两种方法:自动迁移和版本化迁移。