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"
暂无评论