1. 介绍ent
ent是由Facebook开发,专为Go语言设计的实体框架。它简化了大型数据模型应用程序的构建和维护过程。ent框架主要遵循以下原则:
- 将数据库模式轻松建模为图结构。
- 以Go语言代码的形式编程定义模式。
- 根据代码生成实现静态类型。
- 编写数据库查询和图遍历非常简单。
- 使用Go模板简单地进行扩展和自定义。
2. 环境搭建
要开始使用ent框架,首先要确保您的开发环境中安装了Go语言。
如果您的项目目录位于GOPATH
之外,或者您不熟悉GOPATH
,可以使用以下命令建立一个新的Go模块项目:
go mod init entdemo
这会初始化一个新的Go模块,并为您的entdemo
项目创建一个新的go.mod
文件。
3. 定义第一个Schema
3.1. 使用ent CLI创建Schema
首先,需要在项目的根目录下运行以下命令,利用ent的CLI工具创建一个名为User的Schema。
go run -mod=mod entgo.io/ent/cmd/ent new User
上述命令会在entdemo/ent/schema/
目录下生成User的Schema:
文件entdemo/ent/schema/user.go
:
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
}
3.2. 添加字段
接着,我们要在User Schema中添加字段定义,以下是向User实体添加两个字段的示例。
文件修改为entdemo/ent/schema/user.go
:
package schema
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"),
}
}
通过这段代码,我们为User模型定义了两个字段:age
和 name
,其中age
是一个正整数,name
是一个字符串,默认值为”unknown”。
3.3. 生成数据库实体
定义完Schema之后,需要运行go generate
命令来生成底层的数据库访问逻辑。
在项目根目录下运行:
go generate ./ent
该命令会基于之前定义的Schema生成相应的Go代码,并且这个操作会产生以下文件结构:
ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.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
4.1. 初始化数据库连接
要建立与MySQL数据库的连接,我们可以使用ent
框架提供的Open
函数。需要先导入MySQL驱动,然后提供正确的连接字符串来初始化数据库连接。
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/go-sql-driver/mysql" // 导入MySQL驱动
)
func main() {
// 使用ent.Open建立与MYSQL数据库的连接。
// 注意替换下面的用户名"your_username"、密码"your_password"和数据库名"your_database"。
client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
// 运行自动迁移工具
ctx := context.Background()
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// 这里可以写其它的业务逻辑
}
4.2. 创建实体
创建User实体将构建一个新的实体对象,并使用Save
或者SaveX
方法持久化到数据库。以下代码演示了如何创建一个新的User实体,并初始化两个字段age
和name
。
// CreateUser函数用来创建一个新的User实体
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
// 使用client.User.Create()来构建创建User的请求,
// 然后可以链式调用SetAge和SetName方法来设置实体字段的值。
u, err := client.User.
Create().
SetAge(30). // 设置用户年龄
SetName("a8m"). // 设置用户名称
Save(ctx) // 调用Save将实体保存到数据库中
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", u)
return u, nil
}
在main函数中,你可以调用CreateUser
函数来创建新的用户实体。
func main() {
// ...省略建立数据库连接代码
// 创建用户实体
u, err := CreateUser(ctx, client)
if err != nil {
log.Fatalf("failed creating user: %v", err)
}
log.Printf("created user: %#v\n", u)
}
4.3. 查询实体
要查询实体,我们可以使用由ent
生成的查询构建器。以下代码演示了如何查询名为”a8m”的用户:
// QueryUser函数用来查询名为指定名称的用户实体
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
// 使用client.User.Query()来构建查询User的请求
// 然后可以链式调用Where方法来添加查询条件,例如按用户名查询
u, err := client.User.
Query().
Where(user.NameEQ("a8m")). // 添加查询条件,这里是名称等于"a8m"
Only(ctx) // Only方法表示只期望返回一个结果
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}
在main函数中,你可以调用QueryUser
函数来查询用户实体。
func main() {
// ...省略建立数据库连接和创建用户的代码
// 查询用户实体
u, err := QueryUser(ctx, client)
if err != nil {
log.Fatalf("failed querying user: %v", err)
}
log.Printf("queried user: %#v\n", u)
}
5. 添加边缘与反向边缘
5.1. 理解边缘(Edges)和反向边缘(Inverse Edges)
在ent
框架中,数据模型可视化为图结构,其中实体表示图中的节点,实体间的关系则通过边缘(Edges)来表示。边缘是从一个实体指向另一个实体的连接,例如,一个User
可以拥有多辆Car
。
反向边缘(Inverse Edges)是对边缘的反向引用,在逻辑上表示实体间的反向关系,但在数据库中并不创建新的关系。例如,通过Car
的反向边缘,我们可以找到拥有这辆车的User
。
边缘和反向边缘的关键性在于它们使得在相互关联的实体之间导航变得非常直观和简洁。
提示:ent的边缘(Edges),其实就是对应传统数据库的外键,用于定义表之间的关系。
5.2. 在Schema中定义边缘
首先,我们将使用ent
CLI来创建Car
和Group
的初始Schema:
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
接下来,在User
Schema中定义与Car
的边缘,表示用户与汽车之间的关系。我们可以在用户实体里添加一个指向Car
类型的边缘cars
,表明一个用户可以有多辆车:
// entdemo/ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
在定义了边缘后,需要再次运行go generate ./ent
来生成相应的代码。
5.3. 操作边缘数据
创建关联到用户的车辆是一个简单的过程。给定一个用户实体,我们可以创建新的汽车实体,并将其与用户关联起来:
import (
"context"
"log"
"entdemo/ent"
// 确保引入了Car的schema定义
_ "entdemo/ent/schema"
)
func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
user, err := client.User.Get(ctx, userID)
if err != nil {
log.Fatalf("failed getting user: %v", err)
return err
}
// 创建一辆新车并关联到用户
_, err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
SetOwner(user).
Save(ctx)
if err != nil {
log.Fatalf("failed creating car for user: %v", err)
return err
}
log.Println("car was created and associated with the user")
return nil
}
查询用户的车辆同样简单。假设我们想获取一个用户所拥有的所有汽车列表,我们可以这样做:
func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
user, err := client.User.Get(ctx, userID)
if err != nil {
log.Fatalf("failed getting user: %v", err)
return err
}
// 查询用户所拥有的所有汽车
cars, err := user.QueryCars().All(ctx)
if err != nil {
log.Fatalf("failed querying cars: %v", err)
return err
}
for _, car := range cars {
log.Printf("car: %v, model: %v", car.ID, car.Model)
}
return nil
}
通过上述步骤,我们不仅学会了如何在Schema中定义边缘,还演示了如何创建和查询与边缘相关的数据。
6. 图形遍历和查询
6.1. 理解图形结构
在ent中,图形结构是通过实体(Entities)和它们之间的边(Edges)来表示的。每个实体相当于图形中的一个节点(Node),实体之间的关系通过边来表示,可以是一对一(One-to-One)、一对多(One-to-Many)、多对多(Many-to-Many)等。这种图形结构使得在关系型数据库上进行复杂查询和操作变得简单直观。
6.2. 遍历图形结构
编写Graph Traversal代码主要涉及到通过实体之间的边来进行查询和关联数据。下面是一个简单的示例,展示如何遍历ent中的图形结构:
import (
"context"
"log"
"entdemo/ent"
)
// GraphTraversal 遍历图形结构的示例
func GraphTraversal(ctx context.Context, client *ent.Client) error {
// 查询名为"Ariel"的用户
a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
if err != nil {
log.Fatalf("Failed querying user: %v", err)
return err
}
// 遍历Ariel的所有汽车
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
log.Fatalf("Failed querying cars: %v", err)
return err
}
for _, car := range cars {
log.Printf("Ariel has a car with model: %s", car.Model)
}
// 遍历Ariel所在的所有小组
groups, err := a8m.QueryGroups().All(ctx)
if err != nil {
log.Fatalf("Failed querying groups: %v", err)
return err
}
for _, g := range groups {
log.Printf("Ariel is a member of group: %s", g.Name)
}
return nil
}
以上代码就是一个基本的图形遍历例子,首先查询了一个用户,随后遍历了这个用户的汽车和他所在的组。
7. 可视化数据库架构
7.1. 安装Atlas工具
为了可视化由ent生成的数据库架构,我们可以使用Atlas工具。安装Atlas的步骤非常简单,例如在macOS上,可以使用brew
来安装:
brew install ariga/tap/atlas
说明:Atlas是一个通用的数据库迁移工具,可以处理各种数据库的表结构版本管理,后面的章节会详细介绍atlas
7.2. 生成ERD和SQL Schema
使用Atlas查看和导出Schema也非常直观。安装好Atlas后,可通过以下命令查看实体关系图(Entity-Relationship Diagram,ERD):
atlas schema inspect -d [database_dsn] --format dot
或者直接生成SQL Schema:
atlas schema inspect -d [database_dsn] --format sql
其中[database_dsn]
是指向你的数据库的数据源名称(DSN)。例如,对于SQLite数据库可能是:
atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot
这些命令生成的输出可以用相应的工具进一步转换为视图或文档。
8. Schema迁移
8.1. 自动迁移和版本化迁移
ent支持两种schema迁移策略:自动迁移和版本化迁移。自动迁移是在运行时检查和应用schema更改的过程,适合开发和测试使用。版本化迁移则涉及生成迁移脚本,并且需要在生产部署前进行仔细的审核和测试。
提示:自动迁移,参考4.1小结内容。
8.2. 执行版本化迁移
版本化迁移的过程涉及通过Atlas来生成迁移文件,下面是相关的命令:
生成迁移文件:
atlas migrate diff -d ent/schema/path --dir migrations/dir
之后,可以应用这些迁移文件到数据库:
atlas migrate apply -d migrations/dir --url database_dsn
通过这个流程,你可以在版本控制系统中保留数据库迁移的历史记录,并且确保在每次迁移前都能够进行充分的审查。
提示:参考示例代码 https://github.com/ent/ent/tree/master/examples/start