0 0 0

李文周的博客-ORM 框架 ent 介绍

惰卫
2月前 3573

ent  是  Facebook  开源的一款  Go  语言实体框架,是一款简单而强大的用于建模和查询数据的  ORM  框架。

预备知识
图是用来对对象之间的成对关系建模的数学结构,由"节点"或"顶点"(Vertex)以及连接这些顶点的"边"(Edge)组成。

在离散数学中,图(graph)是用于表示物体与物体之间存在某种关系的结构。数学抽象后的“物体”称作节点或顶点(vertex,  node,  point),节点间的相关关系则称作  边(edge)。

图的应用非常广泛,可在物理、生物、社会和信息系统中建模许多类型的关系和过程,许多实际问题可以用图来表示。

例如社交网络中的好友关系、计算机网络连接关系、地图道路等等,图的种类多种多样,根据不同的业务需求,选择不同的图。

ent  介绍
ent  是  Facebook  开源的一款  Go  语言实体框架,是一款简单而强大的用于建模和查询数据的  ORM  框架。

它遵循以下原则,可轻松构建和维护具有大型数据模型的应用程序:

将数据库  schema  建模为图形结构
像写  Go  代码一样定义  schema
基于代码生成的静态类型
便于编写数据库查询和图遍历
使用  Go  模板实现扩展和定制
使用  ent  的过程大致分为以下几步。

先定义好  schema
根据  schema  生成代码
使用生成的  CRUD  代码编写业务代码
安装
执行以下命令安装  ent  cli  工具。

go  install  entgo.io/ent/cmd/ent@latest
接下来,我们将快速开始一个简单示例。

简明示例
创建项目
创建一个  entdemo  项目,执行以下命令完成项目初始化。

go  mod  init  entdemo
创建  schema
在项目的根目录下执行以下命令,创建一个  User  schema。

ent  new  User
该命令会在项目目录下创建一个  ent  目录,其中包含  schema  目录和一个  generate.go  文件。

.
├──  ent
│      ├──  generate.go
│      └──  schema
│              └──  user.go
└──  go.mod
entdemo/ent/schema/user.go  文件中的内容便是定义的  schema。

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  结构体保存实体的  schema  定义。
Fields  方法返回  User  中都有哪些字段。
Edges  方法返回  User  与其他实体的关系。
修改  entdemo/ent/schema/user.go  文件,向  User  的  schema  添加2个字段:

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"),
       }
}
生成代码
在项目根目录执行以下命令,根据上述  schema  生成代码。

go  generate  ./ent
将会生成以下文件。

.
├──  ent
│      ├──  client.go
│      ├──  ent.go
│      ├──  enttest
│      │      └──  enttest.go
│      ├──  generate.go
│      ├──  hook
│      │      └──  hook.go
│      ├──  migrate
│      │      ├──  migrate.go
│      │      └──  schema.go
│      ├──  mutation.go
│      ├──  predicate
│      │      └──  predicate.go
│      ├──  runtime
│      │      └──  runtime.go
│      ├──  runtime.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
├──  go.mod
└──  go.sum
CRUD
使用生成的代码,实现实体的CRUD操作。

ent  支持  SQLite、PostgreSQL、MySQL(MariaDB),本文以  MySQL  为例。

首先,创建一个新客户端来运行  schema  迁移并与实体进行交互:

package  main

import  (
       "context"
       "log"

       "entdemo/ent"

       _  "github.com/go-sql-driver/mysql"
)

func  main()  {
       client,  err  :=  ent.Open("mysql",  "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True")
       if  err  !=  nil  {
               log.Fatalf("failed  opening  connection  to  mysql:  %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)
       }
}
运行  schema  迁移后,我们就可以创建用户了。在本例中,我们将此函数命名为  CreateUser  :

//  CreateUser  创建  user
func  CreateUser(ctx  context.Context,  client  *ent.Client)  (*ent.User,  error)  {
       u,  err  :=  client.User.
               Create().
               SetAge(30).
               SetName("q1mi").
               Save(ctx)
       if  err  !=  nil  {
               return  nil,  fmt.Errorf("failed  creating  user:  %w",  err)
       }
       log.Println("user  was  created:  ",  u)
       return  u,  nil
}
ent为每个实体模式生成一个包,其中包含属性、默认值、验证器和有关存储元素的附加信息(列名、主键等)。

//  QueryUser  查询  user
func  QueryUser(ctx  context.Context,  client  *ent.Client)  (*ent.User,  error)  {
       u,  err  :=  client.User.
               Query().
               Where(user.Name("q1mi")).
               //  `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
}
关联关系
在教程的这一部分,我们要在  schema  中声明与另一个实体的边(关系)。

正向关联
让我们创建两个额外的实体,分别名为  Car  和  Group,并添加一些字段。使用下面的  ent  CLI  命令生成初始  schema  :

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_]+$")),
        }
}
让我们定义第一个关系。User到Car的边定义了一个用户可以拥有  1  辆或多辆汽车,但一辆汽车只有一个车主(一对多关系)。

er-user-cars

让我们将  “Car”  边添加到  User  schema  中,在entdemo/ent/schema/user.go  文件中添加以下内容,并运行  go  generate/ent  生成代码:。

//  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)  {
       //  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)

       //  创建一个User,拥有上面的两辆车。
       alex,  err  :=  client.User.
               Create().
               SetAge(30).
               SetName("alex").
               AddCars(tesla,  ford).
               Save(ctx)
       if  err  !=  nil  {
               return  nil,  fmt.Errorf("failed  creating  user:  %w",  err)
       }
       log.Println("user  was  created:  ",  alex)
       return  alex,  nil
}
想要查询用户的汽车,可以按下面的方式查询。

func  QueryCars(ctx  context.Context,  user  *ent.User)  error  {
       cars,  err  :=  user.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  :=  user.QueryCars().
               Where(car.Model("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

上图中创建的新边是半透明的,以强调我们不会在数据库中创建另一条边。这只是对真实边缘(关系)的反向引用。

让我们在  Car  的  schema  中添加一个名为  owner  的反向边,将其引用到  User  schema  中的  Car边,然后运行  go  generate  ./ent。

//  Edges  of  the  Car.
func  (Car)  Edges()  []ent.Edge  {
       return  []ent.Edge{
               edge.From("owner",  User.Type).
                       //  使用  Ref  方法显式引用  User  表中的  cars
                       Ref("cars").
                       //  设置  Unique,确保一个  Car  只会有一个  owner
                       Unique(),
       }
}
我们将通过查询反向边缘来继续上面的用户/汽车示例。

func  QueryCarUsers(ctx  context.Context,  user  *ent.User)  error  {
       cars,  err  :=  user.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
}
M2M  关联
我们将继续我们的示例,在用户和组之间创建M2M(多对多)关系。

er-group-users

每个  Group  实体可以有多个  User,并且一个  User  可以连接到多个  Group;  这是一个简单的“多对多”关系。在上图中,Group  模式是用户边(关系)的所有者,User  实体对这个名为  Group  的关系具有反向引用/反向边。让我们在  schema  中定义这种关系:

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  文件中添加一个  groups  。

//  Edges  of  the  User.
func  (User)  Edges()  []ent.Edge  {
       return  []ent.Edge{
               edge.To("cars",  Car.Type),
               //  创建一个名为“Group”的反向边,类型为`Group`
               //  显式使用`Ref`方法将其引用到  Group  schema  中定义的  “users”  edge
               edge.From("groups",  Group.Type).
                       Ref("users"),
       }
修改完上述内容后,执行以下命令生成代码。

go  generate  ./ent
查看生成的  schema
要查看  Ent  为数据库生成的  SQL  模式,请安装  Atlas  并运行以下命令:

安装  Atlas
在你的终端执行以下命令。

Mac:

brew  install  ariga/tap/atlas
Windows:

点击下载链接  下载最新版本,然后将其添加到环境变量中。

查看  ent  的  schema
输入以下命令。

atlas  schema  inspect  \
    -u  "ent://ent/schema"  \
    --dev-url  "mysql://<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True"  \
    --format  '{{  sql  .  "    "  }}'
SQL  输出
--  Create  "users"  table
CREATE  TABLE  `users`  (
    `id`  bigint  NOT  NULL  AUTO_INCREMENT,
    `age`  bigint  NOT  NULL,
    `name`  varchar(255)  NOT  NULL  DEFAULT  "unknown",
    PRIMARY  KEY  (`id`)
)  CHARSET  utf8mb4  COLLATE  utf8mb4_bin;
--  Create  "cars"  table
CREATE  TABLE  `cars`  (
    `id`  bigint  NOT  NULL  AUTO_INCREMENT,
    `model`  varchar(255)  NOT  NULL,
    `registered_at`  timestamp  NOT  NULL,
    `user_cars`  bigint  NULL,
    PRIMARY  KEY  (`id`),
    INDEX  `cars_users_cars`  (`user_cars`),
    CONSTRAINT  `cars_users_cars`  FOREIGN  KEY  (`user_cars`)  REFERENCES  `users`  (`id`)  ON  UPDATE  NO  ACTION  ON  DELETE  SET  NULL
)  CHARSET  utf8mb4  COLLATE  utf8mb4_bin;
--  Create  "groups"  table
CREATE  TABLE  `groups`  (
    `id`  bigint  NOT  NULL  AUTO_INCREMENT,
    `name`  varchar(255)  NOT  NULL,
    PRIMARY  KEY  (`id`)
)  CHARSET  utf8mb4  COLLATE  utf8mb4_bin;
--  Create  "group_users"  table
CREATE  TABLE  `group_users`  (
    `group_id`  bigint  NOT  NULL,
    `user_id`  bigint  NOT  NULL,
    PRIMARY  KEY  (`group_id`,  `user_id`),
    INDEX  `group_users_user_id`  (`user_id`),
    CONSTRAINT  `group_users_group_id`  FOREIGN  KEY  (`group_id`)  REFERENCES  `groups`  (`id`)  ON  UPDATE  NO  ACTION  ON  DELETE  CASCADE,
    CONSTRAINT  `group_users_user_id`  FOREIGN  KEY  (`user_id`)  REFERENCES  `users`  (`id`)  ON  UPDATE  NO  ACTION  ON  DELETE  CASCADE
)  CHARSET  utf8mb4  COLLATE  utf8mb4_bin;
遍历图
首先,我们需要按下图生成一些数据(节点和边,或者换句话说,实体和关系)。

re-graph

实际创建数据的代码:

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
}
执行上面的代码后,会生成准备好的数据。我们就可以对它执行一些查询:

获取名为“  GitHub”的组中所有用户的汽车:

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
}
根据  Ariel  查询符合要求的汽车。

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.                                              //  查询  a8m  关联的  groups:
                        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.Model("Mazda"),          //    model  是  "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
}
获取所有有用户的组(查询时使用查找谓词):

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"

最新回复 (0)

    暂无评论

请先登录后发表评论!

返回
请先登录后发表评论!