id: getting-started title: Quick Introduction

sidebar_label: Quick Introduction

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

  • 轻松将你的数据建模为图结构。
  • 使用代码定义模式。
  • 基于代码生成静态类型。
  • 精简的图遍历。


gopher-schema-as-code

安装

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

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

创建第一个模式 (Schema)

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

  1. entc init User

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

  1. // <project>/ent/schema/user.go
  2. package schema
  3. import "github.com/facebookincubator/ent"
  4. // User holds the schema definition for the User entity.
  5. type User struct {
  6. ent.Schema
  7. }
  8. // Fields of the User.
  9. func (User) Fields() []ent.Field {
  10. return nil
  11. }
  12. // Edges of the User.
  13. func (User) Edges() []ent.Edge {
  14. return nil
  15. }

User 模式添加两个字段:

  1. package schema
  2. import (
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/field"
  5. )
  6. // Fields of the User.
  7. func (User) Fields() []ent.Field {
  8. return []ent.Field{
  9. field.Int("age").
  10. Positive(),
  11. field.String("name").
  12. Default("unknown"),
  13. }
  14. }

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

  1. entc generate ./ent/schema

会生成以下文件:

  1. ent
  2. ├── client.go
  3. ├── config.go
  4. ├── context.go
  5. ├── ent.go
  6. ├── example_test.go
  7. ├── migrate
  8. ├── migrate.go
  9. └── schema.go
  10. ├── predicate
  11. └── predicate.go
  12. ├── schema
  13. └── user.go
  14. ├── tx.go
  15. ├── user
  16. ├── user.go
  17. └── where.go
  18. ├── user.go
  19. ├── user_create.go
  20. ├── user_delete.go
  21. ├── user_query.go
  22. └── user_update.go

创建第一个实体(Entity)

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

  1. package main
  2. import (
  3. "log"
  4. "<project>/ent"
  5. _ "github.com/mattn/go-sqlite3"
  6. )
  7. func main() {
  8. client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
  9. if err != nil {
  10. log.Fatalf("failed opening connection to sqlite: %v", err)
  11. }
  12. defer client.Close()
  13. // 运行自动迁移工具。
  14. if err := client.Schema.Create(context.Background()); err != nil {
  15. log.Fatalf("failed creating schema resources: %v", err)
  16. }
  17. }

现在,我们可以创建我们的用户了. 调用函数 CreateUser :

  1. func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
  2. u, err := client.User.
  3. Create().
  4. SetAge(30).
  5. SetName("a8m").
  6. Save(ctx)
  7. if err != nil {
  8. return nil, fmt.Errorf("failed creating user: %v", err)
  9. }
  10. log.Println("user was created: ", u)
  11. return u, nil
  12. }

查询实体

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

  1. package main
  2. import (
  3. "log"
  4. "<project>/ent"
  5. "<project>/ent/user"
  6. )
  7. func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
  8. u, err := client.User.
  9. Query().
  10. Where(user.NameEQ("a8m")).
  11. // `Only` 会查询失败,
  12. // 当未找到用户或找到多个(大于一个)用户时,
  13. Only(ctx)
  14. if err != nil {
  15. return nil, fmt.Errorf("failed querying user: %v", err)
  16. }
  17. log.Println("user returned: ", u)
  18. return u, nil
  19. }

添加第一个边 (关系)

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

  1. entc init Car Group

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

  1. import (
  2. "regexp"
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/field"
  5. )
  6. // Fields of the Car (car.go).
  7. func (Car) Fields() []ent.Field {
  8. return []ent.Field{
  9. field.String("model"),
  10. field.Time("registered_at"),
  11. }
  12. }
  13. // Fields of the Group (group.go).
  14. func (Group) Fields() []ent.Field {
  15. return []ent.Field{
  16. field.String("name").
  17. // 正则验证 group 名.
  18. Match(regexp.MustCompile("[a-zA-Z_]+$")),
  19. }
  20. }

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

er-user-cars

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

  1. import (
  2. "log"
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/edge"
  5. )
  6. // Edges of the User.
  7. func (User) Edges() []ent.Edge {
  8. return []ent.Edge{
  9. edge.To("cars", Car.Type),
  10. }
  11. }

下一个实例: 给一个用户添加两辆车。

  1. func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
  2. // 买一辆 "Tesla".
  3. tesla, err := client.Car.
  4. Create().
  5. SetModel("Tesla").
  6. SetRegisteredAt(time.Now()).
  7. Save(ctx)
  8. if err != nil {
  9. return nil, fmt.Errorf("failed creating car: %v", err)
  10. }
  11. // 买一辆 "Ford".
  12. ford, err := client.Car.
  13. Create().
  14. SetModel("Ford").
  15. SetRegisteredAt(time.Now()).
  16. Save(ctx)
  17. if err != nil {
  18. return nil, fmt.Errorf("failed creating car: %v", err)
  19. }
  20. log.Println("car was created: ", ford)
  21. // 创建一个用户,并给他添加两辆车。
  22. a8m, err := client.User.
  23. Create().
  24. SetAge(30).
  25. SetName("a8m").
  26. AddCars(tesla, ford).
  27. Save(ctx)
  28. if err != nil {
  29. return nil, fmt.Errorf("failed creating user: %v", err)
  30. }
  31. log.Println("user was created: ", a8m)
  32. return a8m, nil
  33. }

怎么查询 cars 的边(关系)呢? 我们是这么做的:

  1. import (
  2. "log"
  3. "<project>/ent"
  4. "<project>/ent/car"
  5. )
  6. func QueryCars(ctx context.Context, a8m *ent.User) error {
  7. cars, err := a8m.QueryCars().All(ctx)
  8. if err != nil {
  9. return fmt.Errorf("failed querying user cars: %v", err)
  10. }
  11. log.Println("returned cars:", cars)
  12. // 筛选特定车型。
  13. ford, err := a8m.QueryCars().
  14. Where(car.ModelEQ("Ford")).
  15. Only(ctx)
  16. if err != nil {
  17. return fmt.Errorf("failed querying user cars: %v", err)
  18. }
  19. log.Println(ford)
  20. return nil
  21. }

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

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

er-cars-owner

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

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

  1. import (
  2. "log"
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/edge"
  5. )
  6. // Edges of the Car.
  7. func (Car) Edges() []ent.Edge {
  8. return []ent.Edge{
  9. // 创建一个类型为 `User` 名为 "owner" 的逆边
  10. // 并且使用 `Ref` 方法明确的将其引用至(User 模式中的) "cars" 边
  11. edge.From("owner", User.Type).
  12. Ref("cars").
  13. // 指定该边为唯一,确保一辆车只有一个车主。
  14. Unique(),
  15. }
  16. }

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

  1. import (
  2. "log"
  3. "<project>/ent"
  4. )
  5. func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
  6. cars, err := a8m.QueryCars().All(ctx)
  7. if err != nil {
  8. return fmt.Errorf("failed querying user cars: %v", err)
  9. }
  10. // 查询逆边。
  11. for _, ca := range cars {
  12. owner, err := ca.QueryOwner().Only(ctx)
  13. if err != nil {
  14. return fmt.Errorf("failed querying car %q owner: %v", ca.Model, err)
  15. }
  16. log.Printf("car %q owner: %q\n", ca.Model, owner.Name)
  17. }
  18. return nil
  19. }

创建第二个边

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

er-group-users

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

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

    1. import (
    2. "log"
    3. "github.com/facebookincubator/ent"
    4. "github.com/facebookincubator/ent/schema/edge"
    5. )
    6. // Edges of the Group.
    7. func (Group) Edges() []ent.Edge {
    8. return []ent.Edge{
    9. edge.To("users", User.Type),
    10. }
    11. }
  • <project>/ent/schema/user.go:

    1. import (
    2. "log"
    3. "github.com/facebookincubator/ent"
    4. "github.com/facebookincubator/ent/schema/edge"
    5. )
    6. // Edges of the User.
    7. func (User) Edges() []ent.Edge {
    8. return []ent.Edge{
    9. edge.To("cars", Car.Type),
    10. // 创建一个类型为 `Group` 名为 "groups" 的逆边
    11. edge.From("groups", Group.Type).
    12. // 并且使用 `Ref` 方法明确的将其引用至(Group 模式中的) "users" 边
    13. Ref("users"),
    14. }
    15. }

运行 entc 重新生成代码。

  1. entc generate ./ent/schema

运行第一个图遍历

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

re-graph

  1. func CreateGraph(ctx context.Context, client *ent.Client) error {
  2. // 首先创建一个用户
  3. a8m, err := client.User.
  4. Create().
  5. SetAge(30).
  6. SetName("Ariel").
  7. Save(ctx)
  8. if err != nil {
  9. return err
  10. }
  11. neta, err := client.User.
  12. Create().
  13. SetAge(28).
  14. SetName("Neta").
  15. Save(ctx)
  16. if err != nil {
  17. return err
  18. }
  19. // 然后,创建汽车,并指定其拥有者(车主)。
  20. _, err = client.Car.
  21. Create().
  22. SetModel("Tesla").
  23. SetRegisteredAt(time.Now()). // 忽略图中的时间
  24. SetOwner(a8m). // 指定车主为 Ariel.
  25. Save(ctx)
  26. if err != nil {
  27. return err
  28. }
  29. _, err = client.Car.
  30. Create().
  31. SetModel("Mazda").
  32. SetRegisteredAt(time.Now()). // 忽略图中的时间
  33. SetOwner(a8m). // 指定车主为 Ariel.
  34. Save(ctx)
  35. if err != nil {
  36. return err
  37. }
  38. _, err = client.Car.
  39. Create().
  40. SetModel("Ford").
  41. SetRegisteredAt(time.Now()). // 忽略图中的时间
  42. SetOwner(neta). // 指定车主为 Neta.
  43. Save(ctx)
  44. if err != nil {
  45. return err
  46. }
  47. // 创建群组,并同时添加用户。
  48. _, err = client.Group.
  49. Create().
  50. SetName("GitLab").
  51. AddUsers(neta, a8m).
  52. Save(ctx)
  53. if err != nil {
  54. return err
  55. }
  56. _, err = client.Group.
  57. Create().
  58. SetName("GitHub").
  59. AddUsers(a8m).
  60. Save(ctx)
  61. if err != nil {
  62. return err
  63. }
  64. log.Println("The graph was created successfully")
  65. return nil
  66. }

现在我们得到了一个有数据的图,我们可以运行一些查询:

  1. 获取 “GitHub” 群组所有用户的全部汽车:

    1. import (
    2. "log"
    3. "<project>/ent"
    4. "<project>/ent/group"
    5. )
    6. func QueryGithub(ctx context.Context, client *ent.Client) error {
    7. cars, err := client.Group.
    8. Query().
    9. Where(group.Name("GitHub")). // (Group(Name=GitHub),)
    10. QueryUsers(). // (User(Name=Ariel, Age=30),)
    11. QueryCars(). // (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    12. All(ctx)
    13. if err != nil {
    14. return fmt.Errorf("failed getting cars: %v", err)
    15. }
    16. log.Println("cars returned:", cars)
    17. // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    18. return nil
    19. }
  2. 修改上面的查询, 将遍历的起源修改为用户 Ariel (Ariel 所属群组的用户的汽车):

    1. import (
    2. "log"
    3. "<project>/ent"
    4. "<project>/ent/car"
    5. )
    6. func QueryArielCars(ctx context.Context, client *ent.Client) error {
    7. // Get "Ariel" from previous steps.
    8. a8m := client.User.
    9. Query().
    10. Where(
    11. user.HasCars(),
    12. user.Name("Ariel"),
    13. ).
    14. OnlyX(ctx)
    15. cars, err := a8m. // 首先获取群组,Ariel 所属的群主为:
    16. QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
    17. QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
    18. QueryCars(). //
    19. Where( //
    20. car.Not( // 获取 Neta 和 Ariel 的汽车
    21. car.ModelEQ("Mazda"), // 但是这里过滤掉了名为 "Mazda" 的汽车
    22. ), //
    23. ). //
    24. All(ctx)
    25. if err != nil {
    26. return fmt.Errorf("failed getting cars: %v", err)
    27. }
    28. log.Println("cars returned:", cars)
    29. // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
    30. return nil
    31. }
  3. 获取有用户(非空)的群组 (使用自动生成的条件查询):

    1. import (
    2. "log"
    3. "<project>/ent"
    4. "<project>/ent/group"
    5. )
    6. func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
    7. groups, err := client.Group.
    8. Query().
    9. Where(group.HasUsers()).
    10. All(ctx)
    11. if err != nil {
    12. return fmt.Errorf("failed getting groups: %v", err)
    13. }
    14. log.Println("groups returned:", groups)
    15. // Output: (Group(Name=GitHub), Group(Name=GitLab),)
    16. return nil
    17. }

完整的例子请参考: GitHub.