快速入门
ent 是一个简单而强大的 Go 实体框架,它使得构建和维护具有大型数据模型的应用程序变得容易,并遵循以下原则:
- 轻松将数据库模式建模为图结构。
- 以编程式 Go 代码定义模式。
- 基于代码生成的静态类型。
- 数据库查询和图遍历易于编写。
- 使用 Go 模板轻松扩展和自定义。

设置 Go 环境
如果你的项目目录不在 GOPATH 中,或者你不熟悉 GOPATH,请按如下方式设置一个 Go 模块 项目:
go mod init entdemo
创建你的第一个模式
进入项目根目录,运行:
go run -mod=mod entgo.io/ent/cmd/ent new User
以上命令将在 entdemo/ent/schema/ 目录下生成 User 的模式:
package schema
import "entgo.io/ent"
// User 保存了 User 实体的模式定义。
type User struct {
ent.Schema
}
// User 的字段。
func (User) Fields() []ent.Field {
return nil
}
// User 的边。
func (User) Edges() []ent.Edge {
return nil
}
向 User 模式添加两个字段:
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
从项目根目录运行 go generate:
go generate ./ent
这将生成以下文件:
ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... 省略
├── schema
│ └── user.go
├── tx.go
├── user
│ ├── user.go
│ └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go
创建你的第一个实体
首先,创建一个新的 Client 来运行模式迁移并与你的实体交互:
- SQLite
- PostgreSQL
- MySQL (MariaDB)
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/mattn/go-sqlite3"
)
func main() {
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()
// 运行自动迁移工具。
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres","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()
// 运行自动迁移工具。
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/go-sql-driver/mysql"
)
func main() {
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()
// 运行自动迁移工具。
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
运行模式迁移后,我们准备创建用户。在此示例中,我们将此函数命名为 CreateUser:
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
}
查询你的实体
ent 为每个实体模式生成一个包,其中包含其谓词、默认值、验证器以及有关存储元素(列名、主键等)的附加信息。
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.Name("a8m")).
// 如果没有找到用户或返回多个用户,`Only` 会失败。
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}
添加你的第一条边(关系)
在本教程的这一部分,我们将在模式中声明到另一个实体的边(关系)。
让我们创建两个名为 Car 和 Group 的附加实体,并添加几个字段。我们使用 ent CLI
生成初始模式:
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
然后我们手动添加其余字段:
// Car 的字段。
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
// Group 的字段。
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// 组名的正则表达式验证。
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}
让我们定义我们的第一个关系。从 User 到 Car 的一条边,定义了一个用户
可以拥有一辆或多辆汽车,但一辆汽车只有一个所有者(一对多关系)。

让我们将 "cars" 边添加到 User 模式,并运行 go generate ./ent:
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
我们继续示例,创建两辆汽车并将它们添加给一个用户。
func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
// 创建一辆 model 为 "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)
// 创建一辆 model 为 "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)
// 创建一个新用户,并将两辆车添加给他。
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 边(关系)呢?我们这样做:
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
}
添加你的第一条反向边(反向引用)
假设我们有一个 Car 对象,我们想获取它的所有者;这辆车属于哪个用户。
为此,我们有另一种称为“反向边”的边,使用 edge.From 函数定义。

上图中创建的新边是半透明的,以强调我们不会在数据库中创建另一条边。它只是对真实边(关系)的反向引用。
让我们向 Car 模式添加一个名为 owner 的反向边,将其引用到 User 模式中的 cars 边,
然后运行 go generate ./ent。
// Car 的边。
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// 创建一个名为 "owner" 的反向边,类型为 `User`
// 并使用 `Ref` 方法显式引用到 "cars" 边(在 User 模式中)。
edge.From("owner", User.Type).
Ref("cars").
// 将边设置为唯一,确保
// 一辆车只能有一个所有者。
Unique(),
}
}
我们继续上面的 user/cars 示例,查询反向边。
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
}
可视化模式
如果你已经到达这一点,你已经成功执行了模式迁移并在数据库中创建了几个实体。要查看 Ent 为数据库生成的 SQL 模式,请安装 Atlas 并运行以下命令:
安装 Atlas
To install the latest release of Atlas, simply run one of the following commands in your terminal, or check out the Atlas website:
- macOS + Linux
- Homebrew
- Docker
- Windows
curl -sSf https://atlasgo.sh | sh
brew install ariga/tap/atlas
docker pull arigaio/atlas
docker run --rm arigaio/atlas --help
If the container needs access to the host network or a local directory, use the --net=host flag and mount the desired
directory:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
Download the latest release and move the atlas binary to a file location on your system PATH.
- ERD Schema
- SQL Schema
检查 Ent 模式
atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
-w
ER 图和 SQL 模式
检查 Ent 模式
atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
--format '{{ sql . " " }}'
SQL 输出
-- 创建 "cars" 表
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
);
-- 创建 "users" 表
CREATE TABLE `users` (
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`age` integer NOT NULL,
`name` text NOT NULL DEFAULT 'unknown'
);
创建你的第二条边
我们将继续示例,在用户和组之间创建 M2M(多对多)关系。

如你所见,每个组实体可以拥有多个用户,一个用户可以连接到多个组;
一个简单的“多对多”关系。在上图中,Group 模式是 users 边(关系)的所有者,
而 User 实体有一个指向此关系的反向引用/反向边,名为 groups。让我们在模式中定义此关系:
// Group 的边。
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
// 创建一个名为 "groups" 的反向边,类型为 `Group`
// 并使用 `Ref` 方法显式引用到 "users" 边(在 Group 模式中)。
edge.From("groups", Group.Type).
Ref("users"),
}
}
我们在模式目录上运行 ent 以重新生成资源。
go generate ./ent
运行你的第一次图遍历
为了运行我们的第一次图遍历,我们需要生成一些数据(节点和边,或者换句话说,实体和关系)。让我们使用框架创建以下图:

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()).
// 将此车附加给 Ariel。
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()).
// 将此车附加给 Ariel。
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
// 将此车附加给 Neta。
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
}
现在我们有了带数据的图,我们可以对其运行一些查询:
获取名为 "GitHub" 的组中所有用户的汽车:
entdemo/start.gofunc 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, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
// 输出: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
return nil
}更改上面的查询,使遍历源为用户 Ariel:
entdemo/start.gofunc QueryArielCars(ctx context.Context, client *ent.Client) error {
// 从之前的步骤中获取 "Ariel"。
a8m := client.User.
Query().
Where(
user.HasCars(),
user.Name("Ariel"),
).
OnlyX(ctx)
cars, err := a8m. // 获取 a8m 连接的组:
QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
QueryCars(). //
Where( //
car.Not( // 获取 Neta 和 Ariel 的汽车,但过滤掉
car.Model("Mazda"), // 名为 "Mazda" 的汽车
), //
). //
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
// 输出: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
return nil
}获取所有拥有用户的组(使用旁视谓词查询):
entdemo/start.gofunc 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
}
模式迁移
Ent 提供了两种运行模式迁移的方法:自动迁移 和 版本化迁移。以下是每种方法的简要概述:
自动迁移
使用自动迁移,用户可以使用以下 API 来保持数据库模式与生成的 SQL 模式 ent/migrate/schema.go 中定义的模式对象一致:
if err := client.Schema.Create(ctx); err != nil {
log.Fatf("failed creating schema resources: %v", err)
}
这种方法主要用于原型设计、开发或测试。因此,建议在任务关键的生产环境中使用 版本化迁移 方法。通过使用版本化迁移,用户可以预先知道将对其数据库应用哪些更改,并且可以根据需要轻松调整它们。
在 自动迁移 文档中阅读更多关于此方法的信息。
版本化迁移
与自动迁移不同,版本迁移方法使用 Atlas 自动生成一组迁移文件,其中包含迁移数据库所需的 SQL 语句。这些文件可以被编辑以满足特定需求,并使用现有的迁移工具(如 Atlas、golang-migrate、Flyway 和 Liquibase)进行应用。此方法的 API 涉及两个主要步骤。
生成迁移
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mysql/8/ent"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mariadb/latest/test"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://postgres/15/test?search_path=public"
atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1"
应用迁移
- MySQL
- MariaDB
- PostgreSQL
- SQLite
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "mysql://root:pass@localhost:3306/example"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "maria://root:pass@localhost:3306/example"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "sqlite://file.db?_fk=1"
在 版本化迁移 文档中阅读更多关于此方法的信息。
完整示例
完整示例存在于 GitHub 上。
