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

安装 Go 环境
如果你的项目目录不在 GOPATH 中或你不熟悉GOPATH,setup a Go module project as follows:
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 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 模式增加两个字段:
// Fields of the 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
... truncated
├── 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()
// Run the auto migration tool.
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()
// Run the auto migration tool.
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()
// Run the auto migration tool.
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` fails if no user found,
// or more than 1 user returned.
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
我们再手动添加剩余字段:
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// Regexp validation for group name.
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}
让我们定义第一个关系。由 User 到 Car 的边定义了一个 user 可以有 一个及以上 car, 但一个 car 只能有一个 所有者(one-to-many 一对多关系)。

让我们添加 "cars" 边到 User 模式中, 添加后运行 go generate ./ent:
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
我们继续在示例中创建两个 car 并将他们添加到 user。
func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Create a new car with 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)
// Create a new car with 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)
// Create a new user, and add it the 2 cars.
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)
// What about filtering specific 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
}
添加第一个反向边(BackRef 反向引用)
假设我们有一个 Car 对象并需获取其所有者——即 car 所属的 user。
为此,我们通过 edge.From 功能定义另一种称为“反向边”的类型。

上面图中新增的边采用半透明效果,旨在强调数据库中并未创建新的边。它仅是对真实边(关系)的反向引用。
让我们为 Car 模式增加一个名为 owner 的反向边, 在 User 模式中将其引用为 cars 边,再运行 go generate ./ent。
// Edges of the Car.
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// Create an inverse-edge called "owner" of type `User`
// and reference it to the "cars" edge (in User schema)
// explicitly using the `Ref` method.
edge.From("owner", User.Type).
Ref("cars").
// setting the edge to unique, ensure
// that a car can have only one owner.
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)
}
// Query the inverse edge.
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
要安装Atlas的最新版本,只需在终端中运行以下任一命令,或访问Atlas 官方网站:
- 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
如果容器需要访问主机网络或本地目录,请使用 --net=host 标志挂载所需目录:
docker run --rm --net=host \
-v $(pwd)/migrations:/migrations \
arigaio/atlas migrate apply
--url "mysql://root:pass@:3306/test"
下载 最新版本 并将 atlas 二进制执行文件所在目录加入到系统路径中。
- ERD Schema
- SQL Schema
检验 Ent 模式
atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
-w
ERD 和 SQL 模式
检验 Ent 模式
atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
--format '{{ sql . " " }}'
SQL 输出
-- Create "cars" table
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 "users" table
CREATE TABLE `users` (
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`age` integer NOT NULL,
`name` text NOT NULL DEFAULT 'unknown'
);
创建第二条边
我们将继续我们的示例,在用户和组之间建立 M2M (多对多)关系。

如您所见,每个 group 实体可 拥有多个 user,而单个 user 也可 关联多个 group —— 这正是典型的"多对多"关系。
在上图示例中,Group 模式是 users 边(关系)的所有者,而 User 实体则通过名为 groups 的反向引用/逆向边与该关系建立关联。现在让我们在模式中定义此关系:
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
// Create an inverse-edge called "groups" of type `Group`
// and reference it to the "users" edge (in Group schema)
// explicitly using the `Ref` method.
edge.From("groups", Group.Type).
Ref("users"),
}
}
我们在模式目录中运行 ent 重新生成相应资源。
go generate ./ent
运行你的第一个图遍历
为了运行我们的首次图遍历,我们需要生成一些数据(节点和边,或者说实体和关系)。让我们使用该框架创建以下图:

func CreateGraph(ctx context.Context, client *ent.Client) error {
// First, create the users.
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
}
// Then, create the cars, and attach them to the users created above.
err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
// Attach this car to Ariel.
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()).
// Attach this car to Ariel.
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
// Attach this car to Neta.
SetOwner(neta).
Exec(ctx)
if err != nil {
return err
}
// Create the groups, and add their users in the creation.
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" 的 group 中获取全部 user 的 car:
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)
// Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
return nil
} -
修改上述查询,遍历名称为 Ariel 的 user:
entdemo/start.gofunc QueryArielCars(ctx context.Context, client *ent.Client) error {
// Get "Ariel" from previous steps.
a8m := client.User.
Query().
Where(
user.HasCars(),
user.Name("Ariel"),
).
OnlyX(ctx)
cars, err := a8m. // Get the groups, that a8m is connected to:
QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
QueryCars(). //
Where( //
car.Not( // Get Neta and Ariel cars, but filter out
car.Model("Mazda"), // those who named "Mazda"
), //
). //
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
// Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
return nil
} -
获取具有 user 的全部 group (使用旁路谓词进行查询):
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)
// Output: (Group(Name=GitHub), Group(Name=GitLab),)
return nil
}
模式迁移
Ent 提供两重模式迁移方法:自动迁移 和 版本化迁移. 以下是对每种版本的简要概述:
自动迁移
通过自动迁移功能,用户可使用以下API使数据库模式与生成的SQL模式 ent/migrate/schema.go 中定义的模式对象保持一致:
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("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 中查看全部示例。
