快速入门

ent 是一个基于 SQL/Gremlin 构建的易于使用但功能强大的 Go Entity 框架,其遵循以下原则:

  • 轻松将你的数据建模为图结构。

  • 使用代码定义模式。

  • 基于代码生成静态类型。

  • 精简的图遍历。

安装

go get github.com/facebookincubator/ent/cmd/entc

完成 entc (为 ent 生成代码) 的安装后, 你应该将其放入 PATH 中。

创建第一个模式 (Schema)

进行你的项目根目录,并运行命令:

entc init User

上面的命令将在 <project>/ent/schema/ 目录下为 User 生成模式.

// <project>/ent/schema/user.go

package schema

import "github.com/facebookincubator/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 模式添加两个字段:

package schema

import (
    "github.com/facebookincubator/ent"
    "github.com/facebookincubator/ent/schema/field"
)


// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

在项目根目录运行命令 entc generate:

entc generate ./ent/schema

会生成以下文件:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── example_test.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

创建第一个实体(Entity)

首先, 创建一个新的 ent.Client. 在这个例子中,我们将使用 SQLite3.

package main

import (
    "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()
    // 运行自动迁移工具。
    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: %v", err)
    }
    log.Println("user was created: ", u)
    return u, nil
}

查询实体

entc 会为每个实体的模式生成到一个包内,并包含条件,默认值,验证器和存储相关的附加信息(列名,主键等)。

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: %v", err)
    }
    log.Println("user returned: ", u)
    return u, nil
}

添加第一个边 (关系)

在教程的这部分,我们要声明 (关系) 到模式的另一个实体。 让我们创建另外两个名为 CarGroup 且有一些字段的实体。 我们使用 entc 去生成初始模式。

entc init Car Group

然后我们手动添加剩下的字段:

import (
    "regexp"

    "github.com/facebookincubator/ent"
    "github.com/facebookincubator/ent/schema/field"
)

// Fields of the Car (car.go). 
func (Car) Fields() []ent.Field {
    return []ent.Field{
        field.String("model"),
        field.Time("registered_at"),
    }
}


// Fields of the Group (group.go).
func (Group) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // 正则验证 group 名.
            Match(regexp.MustCompile("[a-zA-Z_]+$")),
    }
}

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

让我们添加 "cars" 的边到 User 的模式中, 然后运行 entc generate ./ent/schema:

 import (
     "log"

     "github.com/facebookincubator/ent"
     "github.com/facebookincubator/ent/schema/edge"
 )

 // Edges of the 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) {
    // 买一辆 "Tesla".
    tesla, err := client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed creating car: %v", err)
    }

    // 买一辆 "Ford".
    ford, err := client.Car.
        Create().
        SetModel("Ford").
        SetRegisteredAt(time.Now()).
        Save(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed creating car: %v", 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: %v", 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: %v", 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: %v", err)
    }
    log.Println(ford)
    return nil
}

添加第一个逆边(反向引用)

假设我们有一个 Car 对象,并且我们想知道它的车主;即 Car 属于哪个 User. 对于这种情况,我们有另一种叫做 “逆边” 的边类型,他的定义函数是 edge.From.

上图中半透明部分就是新的边,要强调的是,我们不会在数据库中创建这条边 它只是对上面的边的反向引用。

让我们为 Car 模式添加一个叫 owner 的逆边,将其引用至 User 模式中的 cars 边 然后运行 entc generate ./ent/schema.

import (
    "log"

    "github.com/facebookincubator/ent"
    "github.com/facebookincubator/ent/schema/edge"
)

// Edges of the Car.
func (Car) Edges() []ent.Edge {
    return []ent.Edge{
        // 创建一个类型为 `User` 名为 "owner" 的逆边
        // 并且使用 `Ref` 方法明确的将其引用至(User 模式中的) "cars" 边
         edge.From("owner", User.Type).
             Ref("cars").
            // 指定该边为唯一,确保一辆车只有一个车主。
            Unique(),
    }
}

接着上面的 user/cars 例子,我们来查询逆边。

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

创建第二个边

继续看例子,我们将在 users 和 groups 之间创建一个 M2M (多对多)的关系。

如图所示,每个群组实体可以 拥有多个 用户,一个用户也可以 被连接到多个 群组,一个简单的 “多对多” 关系。 在上图中,Group 模式是 users 边(关系)的拥有者, User 实体有一个名为 groups 的反向引用/逆边。 开始定义这个多对多关系:

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

       import (
          "log"
    
          "github.com/facebookincubator/ent"
          "github.com/facebookincubator/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"
    
           "github.com/facebookincubator/ent"
           "github.com/facebookincubator/ent/schema/edge"
       )
    
       // Edges of the User.
       func (User) Edges() []ent.Edge {
           return []ent.Edge{
              edge.To("cars", Car.Type),
               // 创建一个类型为 `Group` 名为 "groups" 的逆边
              edge.From("groups", Group.Type).
                //  并且使用 `Ref` 方法明确的将其引用至(Group 模式中的) "users" 边
                  Ref("users"),
           }
       }

运行 entc 重新生成代码。

entc generate ./ent/schema

运行第一个图遍历

为了运行第一个图遍历,我们需要生成一些数据(节点和边,或者说实体和关系)。 让我们使用 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()). // 忽略图中的时间
        SetOwner(a8m).               // 指定车主为 Ariel.
        Save(ctx)
    if err != nil {
        return err
    }
    _, err = client.Car.
        Create().
        SetModel("Mazda").
        SetRegisteredAt(time.Now()). // 忽略图中的时间
        SetOwner(a8m).               // 指定车主为 Ariel.
        Save(ctx)
    if err != nil {
        return err
    }
    _, err = client.Car.
        Create().
        SetModel("Ford").
        SetRegisteredAt(time.Now()). // 忽略图中的时间
        SetOwner(neta).              // 指定车主为 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: %v", err)
         }
         log.Println("cars returned:", cars)
         // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
         return nil
     }
  2. 修改上面的查询, 将遍历的起源修改为用户 Ariel (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.                         // 首先获取群组,Ariel 所属的群主为:
                 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.ModelEQ("Mazda"),    //    但是这里过滤掉了名为 "Mazda" 的汽车
                     ),                             //
                 ).                                 //
                 All(ctx)
         if err != nil {
             return fmt.Errorf("failed getting cars: %v", err)
         }
         log.Println("cars returned:", cars)
         // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
         return nil
     }
  3. 获取有用户(非空)的群组 (使用自动生成的条件查询):

     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: %v", err)
         }
         log.Println("groups returned:", groups)
         // Output: (Group(Name=GitHub), Group(Name=GitLab),)
         return nil
     }

完整的例子请参考: GitHub.

Last updated