跳到主要内容

快速指南

ent 是一个简单而又强大的Go实体框架,便于构建和使用大型数据模型时能够遵循以下原则:

  • 简单地使用数据库结构作为图结构。
  • 使用Go代码定义结构。
  • 基于代码生成的静态类型。
  • 容易地进行数据库查询和图遍历。
  • 容易地使用Go模板扩展和自定义。

gopher-schema-as-code

安装#

go get entgo.io/ent/cmd/ent

在安装 ent 代码生成(codegen) 工具后,你应能在 PATH 中找到它。 如果你找不到路径,你也可以运行: go run entgo.io/ent/cmd/ent <command>

设置Go环境#

如果你的项目目录在 GOPATH 之外 或 不熟悉 GOPATH, 可以为项目设置 Go module

go mod init <project>

创建你的第一个结构#

转到项目的根目录并运行:

go run entgo.io/ent/cmd/ent init User

此命令将生成结构 User<project>/ent/schema/ 目录内:

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

将 2 个字段添加到 User 结构:

package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// 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
├── migrate
│ ├── migrate.go
│ └── schema.go
├── predicate
│ └── predicate.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

创建你的第一个实体#

创建一个 ent.Client。 作为例子,我们将使用 SQLite3。

package main
import (
"context"
"log"
"<project>/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 函数,比如:

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 为每个实体结构生成一个package,包含其条件、默认值、验证器、有关存储元素 (列名、主键等) 的额外信息。

package main
import (
"log"
"<project>/ent"
"<project>/ent/user"
)
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.NameEQ("a8m")).
// `Only` 在 找不到用户 或 找到多于一个用户 时报错,
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}

添加你的第一条边 (关系)#

在这部分教程中,我们想声明一条到另一个 实体 的 边(关系) 。 让我们另外创建2个实体,分别为 CarGroup,并添加一些字段。 我们使用 ent CLI 生成初始的结构(schema):

go run entgo.io/ent/cmd/ent init Car Group

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

import (
"regexp"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// 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_]+$")),
}
}

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

er-user-cars

让我们将 "cars" 关系添加到 User 结构中,并运行 go generate ./ent

import (
"log"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}

作为示例,我们创建2辆汽车并将它们添加到某个用户。

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)
// 新建一个用户,将两辆车添加到他的名下
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 关系怎么办? 请参考以下:

import (
"log"
"<project>/ent"
"<project>/ent/car"
)
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.ModelEQ("Ford")).
Only(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println(ford)
return nil
}

添加您的第一条逆向边(反向引用)#

假定我们有一个 Car 对象,我们想要得到它的所有者;即这辆汽车所属的用户。 为此,我们有另一种“逆向”的边,通过 edge.From 函数定义。

er-cars-owner

在上图中新建的边是隐性的,以强调我们不会在数据库中创建另一个关联。 它只是真正边(关系) 的回溯。

让我们把一个名为 owner 的逆向边添加到 Car 的结构中, 在 User 结构中引用它到 cars 关系 然后运行 go generate ./ent

import (
"log"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// 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").
// 将这条边设置为unique(唯一的)
// 确保每辆车只有一位车主
Unique(),
}
}

我们继续使用用户和汽车,作为查询逆向边的例子。

import (
"fmt"
"log"
"<project>/ent"
)
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 _, ca := range cars {
owner, err := ca.QueryOwner().Only(ctx)
if err != nil {
return fmt.Errorf("failed querying car %q owner: %w", ca.Model, err)
}
log.Printf("car %q owner: %q\n", ca.Model, owner.Name)
}
return nil
}

创建你的第二条边#

继续我们的例子,在用户和组之间创建一个M2M(多对多)关系。

er-group-users

正如您所看到的,每个组实体可以拥有许多用户,而一个用户可以关联到多个组。 一个简单的 "多对多 "关系。 在上面的插图中,Group结构是users关系的所有者, 而User实体对这个关系有一个名为groups的反向引用。 让我们在结构中定义这种关系。

  • <project>/ent/schema/group.go:

    import (
    "log"
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    )
    // Edges of the Group.
    func (Group) Edges() []ent.Edge {
    return []ent.Edge{
    edge.To("users", User.Type),
    }
    }
  • <project>/ent/schema/user.go:

    import (
    "log"
    "entgo.io/ent"
    "entgo.io/ent/schema/edge"
    )
    // 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"),
    }
    }

我们在schema目录上运行ent来重新生成资源文件。

go generate ./ent

进行第一次图遍历#

为了进行我们的第一次图遍历,我们需要生成一些数据(节点和边,或者说,实体和关系)。 让我们创建如下图所示的框架:

re-graph

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()). // ignore the time in the graph.
SetOwner(a8m). // attach this graph to Ariel.
Save(ctx)
if err != nil {
return err
}
_, err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()). // ignore the time in the graph.
SetOwner(a8m). // attach this graph to Ariel.
Save(ctx)
if err != nil {
return err
}
_, err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()). // ignore the time in the graph.
SetOwner(neta). // attach this graph to Neta.
Save(ctx)
if err != nil {
return err
}
// 创建用户组,并在创建之初就为他们添加一些用户
_, err = client.Group.
Create().
SetName("GitLab").
AddUsers(neta, a8m).
Save(ctx)
if err != nil {
return err
}
_, err = client.Group.
Create().
SetName("GitHub").
AddUsers(a8m).
Save(ctx)
if err != nil {
return err
}
log.Println("The graph was created successfully")
return nil
}

现在我们有一个含数据的图,我们可以对它运行一些查询:

  1. 获取名为 "GitHub" 的群组内所有用户的汽车。

    import (
    "log"
    "<project>/ent"
    "<project>/ent/group"
    )
    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 开始遍历。

    import (
    "log"
    "<project>/ent"
    "<project>/ent/car"
    )
    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.ModelEQ("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. 获取所有拥有用户的群组 (通过额外 [look-aside] 条件查询):

    import (
    "log"
    "<project>/ent"
    "<project>/ent/group"
    )
    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
    }

完整示例请参阅 GitHub.