跳到主要内容

快速介绍

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

  • 轻松将数据库模式建模为图结构。
  • 通过程序化的Go代码定义模式。
  • 基于代码生成的静态类型。
  • 轻松编写数据库查询与图遍历。
  • 使用Go模板实现简单扩展与定制。

gopher-schema-as-code

安装 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 模式:

entdemo/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 模式增加两个字段:

entdemo/ent/schema/user.go
// 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 进行模式迁移并与你的实体进行交互:

entdemo/start.go
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)
}
}

在运行完模式迁移后,就可以创建用户了。 在本例中,我们将函数命名为 CreateUser

entdemo/start.go
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 为每个实体模式生成一个包,包含了图标、默认值、校验和存储元素的附加信息(列名、主键等)。

entdemo/start.go
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
}

添加第一条边(关系)

在本教程的这一部分,我们将声明一个指向模式中另一个实体的边(关系)。 让我们创建另外两个名为 CarGroup 的实体,并添加几个字段。使用 ent CLI 生成初始模式:

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

我们再手动添加剩余字段:

entdemo/ent/schema/car.go
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
entdemo/ent/schema/group.go
// 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_]+$")),
}
}

让我们定义第一个关系。由 UserCar 的边定义了一个 user 可以有 一个及以上 car, 但一个 car 只能有一个 所有者(one-to-many 一对多关系)。

er-user-cars

让我们添加 "cars" 边到 User 模式中, 添加后运行 go generate ./ent

entdemo/ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}

我们继续在示例中创建两个 car 并将他们添加到 user。

entdemo/start.go
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 的边(关系)? 我们可以这样做:

entdemo/start.go
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 功能定义另一种称为“反向边”的类型。

er-cars-owner

上面图中新增的边采用半透明效果,旨在强调数据库中并未创建新的边。它仅是对真实边(关系)的反向引用。

让我们为 Car 模式增加一个名为 owner 的反向边, 在 User 模式中将其引用为 cars 边,再运行 go generate ./ent

entdemo/ent/schema/car.go
// 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 的示例,查询反向边。

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)
}
// 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 官方网站

curl -sSf https://atlasgo.sh | sh

检验 Ent 模式

atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
-w

ERD 和 SQL 模式

erd

创建第二条边

我们将继续我们的示例,在用户和组之间建立 M2M (多对多)关系。

er-group-users

如您所见,每个 group 实体可 拥有多个 user,而单个 user 也可 关联多个 group —— 这正是典型的"多对多"关系。 在上图示例中,Group 模式是 users 边(关系)的所有者,而 User 实体则通过名为 groups 的反向引用/逆向边与该关系建立关联。现在让我们在模式中定义此关系:

entdemo/ent/schema/group.go
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
entdemo/ent/schema/user.go
// 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

运行你的第一个图遍历

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

re-graph

entdemo/start.go
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
}

现在我们有了图的数据,并在此基础上运行几个查询:

  1. 我们在名为 "GitHub" 的 group 中获取全部 user 的 car:

    entdemo/start.go
    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, 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
    }
  2. 修改上述查询,遍历名称为 Ariel 的 user:

    entdemo/start.go
    func 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
    }
  3. 获取具有 user 的全部 group (使用旁路谓词进行查询):

    entdemo/start.go
    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)
    // 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涉及两个主要步骤。

生成迁移

atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mysql/8/ent"

应用迁移

atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "mysql://root:pass@localhost:3306/example"

版本化迁移 文档中阅读此方法的更多内容。

全部示例

GitHub 中查看全部示例。