快速指南

ent 是一个简单而又强大的Go实体框架,易于构建和维护应用程序与大数据模型:

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

gopher-schema-as-code

初始化Go环境

如果您的项目位于 GOPATH 之外或者不熟悉 GOPATH, 您可以像这样使用 Go module

  1. go mod init <project>

安装

  1. go get entgo.io/ent/cmd/ent

在安装 ent 代码生成工具后,您应该在 PATH 中看到它。 如果您没看到,则应该运行命令:go run entgo.io/ent/cmd/ent <command>

创建你的第一个结构

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

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

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

<project>/ent/schema/user.go

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

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

<project>/ent/schema/user.go

  1. package schema
  2. import (
  3. "entgo.io/ent"
  4. "entgo.io/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. }

在项目根目录中运行 go generate 如下:

  1. go generate ./ent

这将创建以下文件:

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

创建你的第一个实体

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

<project>/start/start.go

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

现在,我们做好了创建用户的准备。 让我们调用 CreateUser 函数,比如:

<project>/start/start.go

  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: %w", err)
  9. }
  10. log.Println("user was created: ", u)
  11. return u, nil
  12. }

查询你的实体

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

<project>/start/start.go

  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.Name("a8m")).
  11. // `Only` 在 找不到用户 或 找到多于一个用户 时报错,
  12. Only(ctx)
  13. if err != nil {
  14. return nil, fmt.Errorf("failed querying user: %w", err)
  15. }
  16. log.Println("user returned: ", u)
  17. return u, nil
  18. }

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

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

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

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

<project>/ent/schema/car.go

  1. // Fields of the Car.
  2. func (Car) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.String("model"),
  5. field.Time("registered_at"),
  6. }
  7. }

<project>/ent/schema/group.go

  1. // Fields of the Group.
  2. func (Group) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.String("name").
  5. // 使用正则表达式校验 group name
  6. Match(regexp.MustCompile("[a-zA-Z_]+$")),
  7. }
  8. }

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

er-user-cars

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

<project>/ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("cars", Car.Type),
  5. }
  6. }

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

<project>/start/start.go

  1. import (
  2. "<project>/ent"
  3. "<project>/ent/car"
  4. "<project>/ent/user"
  5. )
  6. func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
  7. // 创建一辆车型为 "Tesla" 的汽车.
  8. tesla, err := client.Car.
  9. Create().
  10. SetModel("Tesla").
  11. SetRegisteredAt(time.Now()).
  12. Save(ctx)
  13. if err != nil {
  14. return nil, fmt.Errorf("failed creating car: %w", err)
  15. }
  16. log.Println("car was created: ", tesla)
  17. // 创建一辆车型为 "Ford" 的汽车.
  18. ford, err := client.Car.
  19. Create().
  20. SetModel("Ford").
  21. SetRegisteredAt(time.Now()).
  22. Save(ctx)
  23. if err != nil {
  24. return nil, fmt.Errorf("failed creating car: %w", err)
  25. }
  26. log.Println("car was created: ", ford)
  27. // 新建一个用户,将两辆车添加到他的名下
  28. a8m, err := client.User.
  29. Create().
  30. SetAge(30).
  31. SetName("a8m").
  32. AddCars(tesla, ford).
  33. Save(ctx)
  34. if err != nil {
  35. return nil, fmt.Errorf("failed creating user: %w", err)
  36. }
  37. log.Println("user was created: ", a8m)
  38. return a8m, nil
  39. }

想要查询 cars 关系怎么办? 请参考以下:

<project>/start/start.go

  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: %w", err)
  10. }
  11. log.Println("returned cars:", cars)
  12. // What about filtering specific cars.
  13. ford, err := a8m.QueryCars().
  14. Where(car.Model("Ford")).
  15. Only(ctx)
  16. if err != nil {
  17. return fmt.Errorf("failed querying user cars: %w", err)
  18. }
  19. log.Println(ford)
  20. return nil
  21. }

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

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

er-cars-owner

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

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

<project>/ent/schema/car.go

  1. // Edges of the Car.
  2. func (Car) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. // Create an inverse-edge called "owner" of type `User`
  5. // and reference it to the "cars" edge (in User schema)
  6. // explicitly using the `Ref` method.
  7. edge.From("owner", User.Type).
  8. Ref("cars").
  9. // setting the edge to unique, ensure
  10. // that a car can have only one owner.
  11. Unique(),
  12. }
  13. }

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

<project>/start/start.go

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

创建你的第二条边

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

er-group-users

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

<project>/ent/schema/group.go

  1. // Edges of the Group.
  2. func (Group) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("users", User.Type),
  5. }
  6. }

<project>/ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("cars", Car.Type),
  5. // Create an inverse-edge called "groups" of type `Group`
  6. // and reference it to the "users" edge (in Group schema)
  7. // explicitly using the `Ref` method.
  8. edge.From("groups", Group.Type).
  9. Ref("users"),
  10. }
  11. }

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

  1. go generate ./ent

进行第一次图遍历

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

re-graph

<project>/start/start.go

  1. func CreateGraph(ctx context.Context, client *ent.Client) error {
  2. // First, create the users.
  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. // Then, create the cars, and attach them to the users in the creation.
  20. err = client.Car.
  21. Create().
  22. SetModel("Tesla").
  23. SetRegisteredAt(time.Now()). // ignore the time in the graph.
  24. SetOwner(a8m). // attach this graph to Ariel.
  25. Exec(ctx)
  26. if err != nil {
  27. return err
  28. }
  29. err = client.Car.
  30. Create().
  31. SetModel("Mazda").
  32. SetRegisteredAt(time.Now()). // ignore the time in the graph.
  33. SetOwner(a8m). // attach this graph to Ariel.
  34. Exec(ctx)
  35. if err != nil {
  36. return err
  37. }
  38. err = client.Car.
  39. Create().
  40. SetModel("Ford").
  41. SetRegisteredAt(time.Now()). // ignore the time in the graph.
  42. SetOwner(neta). // attach this graph to Neta.
  43. Exec(ctx)
  44. if err != nil {
  45. return err
  46. }
  47. // Create the groups, and add their users in the creation.
  48. err = client.Group.
  49. Create().
  50. SetName("GitLab").
  51. AddUsers(neta, a8m).
  52. Exec(ctx)
  53. if err != nil {
  54. return err
  55. }
  56. err = client.Group.
  57. Create().
  58. SetName("GitHub").
  59. AddUsers(a8m).
  60. Exec(ctx)
  61. if err != nil {
  62. return err
  63. }
  64. log.Println("The graph was created successfully")
  65. return nil
  66. }

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

  1. 获取名为 “GitHub” 的群组内所有用户的汽车。 <project>/start/start.go

    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: %w", 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 开始遍历。

    <project>/start/start.go

    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. // Get the groups, that a8m is connected to:
    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( // Get Neta and Ariel cars, but filter out
    21. car.Model("Mazda"), // those who named "Mazda"
    22. ), //
    23. ). //
    24. All(ctx)
    25. if err != nil {
    26. return fmt.Errorf("failed getting cars: %w", 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. 获取所有拥有用户的群组 (通过额外 [look-aside] 条件查询):

    <project>/start/start.go

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

完整示例

完整示例请参阅 GitHub.