ent ORM框架入门教程
前言
ent是一个由Facebook开源的go语言ORM框架,其使用代码定义数据库schema,并通过代码生成的方式来构建静态类型的数据模型和API方法。与采用反射实现的ORM框架相比,避免了反射带来的性能和代码可读性等问题,同时代码也更为优雅。
ent
的文档还是挺完善的,本教程选取了若干常用的功能,将他们从分散的文档中整理成此篇入门教程。若需进一步了解对应功能,推荐参阅官方文档。
通过本教程,您将了解以下内容:
- 基础schema定义,与CRUD API的基本用法。
- 常用关联关系定义,与查询基本方法。包括定义一对多,多对多关联关系。关联关系查询,以及即时加载。
- 钩子与拦截器的基本使用方法,并基于它们实现软删除。
- 如何在
ent
中使用事务
示例代码可以在这里找到:github.com/DrmagicE/en…
快速开始
快速开始章节基于官方文档做了简化,更详细的内容可参阅官方文档:entgo.io/docs/gettin…
使用下列命令新建工程目录entdemo
,初始化项目环境,并安装ent
命令行工具:
mkdir entdemo
cd entdemo
go mod init entdemo
go install entgo.io/ent/cmd/ent@v0.12.2
go get entgo.io/ent@v0.12.2
ent
使用了泛型,确保当前环境Go版本 > 1.18。本教程使用的Go版本为 v1.19.3
定义schema
在本教程中,将使用User
,Group
,Car
(用户,用户组,车)三个模型为示例。
使用ent new
命令创建这三个模型的schema:
ent new User Group Car
创建成功后,ent
将为我们在 ent/schema
目录下生成三个schema文件,每个schema文件对应数据库一张表。
$ ls ent/schema
car.go group.go user.go
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.
// Fields 用于定义User表字段。
func (User) Fields() []ent.Field {
return nil
}
// Edges of the User.
// Edges 用于定义关联关系。
func (User) Edges() []ent.Edge {
return nil
}
其中Fields()
方法和Edges()
方法分别用于定义User
表的字段和关联关系。
分别修改user.go``car.go``group.go
三个文件中的Fields()
,增加一个string类型的name
字段——表示我们为各自表增加一个string类型的name数据库字段。
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}
修改后,运行下列命令生成代码:
go generate ./ent
创建&查询实例
接下来,我们以 sqlite3
数据库(内存模式)为例,使用ent.Client
来建表和创建查询实例。
创建entdemo/start.go
文件:
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/user"
_ "github.com/mattn/go-sqlite3"
)
func main() {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
// client.Schema.Create 会自动建表,示例中使用内存模式的数据库,所以每次都会重新建表。
if err := client.Schema.Create(
context.Background(),
// ent默认会为关联字段创建外键,按使用习惯决定是否增加这行配置禁用外键。
migrate.WithForeignKeys(false),
); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
CreateAndQueryUser(client)
}
func CreateAndQueryUser(client *ent.Client) {
// 创建 name="user1"的用户
client.Debug().User.Create().SetName("user1").SaveX(context.Background())
// 查询 name="user1" 的用户
fmt.Println(client.Debug().User.Query().Where(user.Name("user1")).All(context.Background()))
}
tips:
client.Debug()
会打印执行的数据库语句,可以更直观的体现ent
做了哪些数据库操作。
运行上述程序,得到输出:
2023/05/01 20:32:58 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user1]
User(id=3, name=user1)
2023/05/01 20:32:58 driver.Query: query=SELECT `users`.`id`, `users`.`name` FROM `users` WHERE `users`.`name` = ? args=[user1]
[User(id=1, name=user1) User(id=2, name=user1) User(id=3, name=user1)] <nil>
更详细的CRUD API,可参考文档:entgo.io/docs/crud
关联关系
在本文示例中,User
,Group
,Car
有以下关联关系:
即:
- 用户可以拥有多辆汽车,但一辆汽车只能属于一个用户。用户和车为一对多(One-to-Many)关系。
- 用户组可以包含多个用户,同时一个用户可以加入多个用户组。用户和用户组为多对多(Many-to-Many)关系。
ent
使用图的概念来表达数据之间的关联模型,在ent
中,关联关系使用edge
(边)来定义。
一对多关系
修改user.go
的Edges()
方法,增加edge.To
表达关联关系:
// entdemo/ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
// 定义一条名为"cars"的,指向Car类型的边
edge.To("cars", Car.Type),
}
}
上面定义了 User->Car (1:M) 这条边,类似的,我们修改car.go
方法,增加edge.From
表达 Car -> User (M:1) 的反向边。
// entdemo/ent/schema/car.go
// Edges of the Car.
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// 创建一个指向User类型的反向关联关系”owner“。
// 通过Ref方法,显示的将其与在User中定义的“cars”关联关系关联。
edge.From("owner", User.Type).
Ref("cars").
// 通过Unique表达Car只能属于一个User。
// (如果不加Unique,那表达的就是多对多的关系了)
Unique(),
}
}
修改后,运行下列命令生成代码:
go generate ./ent
定义好关联关系后,我们可以:
- 在创建
User
时,关联属于他的Car
- 也可以在创建
Car
时,指定Car
的Owner
以上两种用法示例如下:
// entdemo/start.go
// 创建User时,关联属于他的Car
func AddCarsToUser(client *ent.Client) {
// 创建2辆车
car1 := client.Debug().Car.Create().SetName("car1").SaveX(context.Background())
car2 := client.Debug().Car.Create().SetName("car2").SaveX(context.Background())
// 这两辆车属于 name="user2"的用户
client.Debug().User.Create().SetName("user2").AddCars(car1, car2).SaveX(context.Background())
}
// 创建Car时,指定Car的Owner
func SetOwnerToCar(client *ent.Client) {
// 创建一个用户
user := client.Debug().User.Create().SetName("user2").SaveX(context.Background())
// 创建车,并绑定用户
client.Debug().Car.Create().SetName("car3").SetOwner(user).SaveX(context.Background())
}
以上两个方法的输出分别为:
# AddCarsToUser
2023/05/01 20:47:02 driver.Query: query=INSERT INTO `cars` (`name`) VALUES (?) RETURNING `id` args=[car1]
2023/05/01 20:47:02 driver.Query: query=INSERT INTO `cars` (`name`) VALUES (?) RETURNING `id` args=[car2]
2023/05/01 20:47:02 driver.Tx(7633b9ef-56db-4037-a14b-c57417f0c774): started
2023/05/01 20:47:02 Tx(7633b9ef-56db-4037-a14b-c57417f0c774).Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user2]
2023/05/01 20:47:02 Tx(7633b9ef-56db-4037-a14b-c57417f0c774).Exec: query=UPDATE `cars` SET `user_cars` = ? WHERE `id` IN (?, ?) AND `user_cars` IS NULL args=[1 1 2]
2023/05/01 20:47:02 Tx(7633b9ef-56db-4037-a14b-c57417f0c774): committed
# AddUserToCar
2023/05/01 20:47:29 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user2]
2023/05/01 20:47:29 driver.Query: query=INSERT INTO `cars` (`name`, `user_cars`) VALUES (?, ?) RETURNING `id` args=[car3 1]
根据日志可以发现,ent
在cars
表中定义了user_cars
字段来表达关联关系。
多对多关系
接下来我们创建User
和Group
的多对多关系。
分别修改entdemo/ent/schema/user.go
和entdemo/ent/schema/group.go
文件
// entdemo/ent/schema/user.go
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
// 定义一条名为"users",指向User类型的边
edge.To("users", User.Type),
}
}
// entdemo/ent/schema/group.go
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
// 创建一个指向Group类型的反向关联边"groups"。
// 通过Ref方法,显示的将其与在Group中定义的“users”边关联。
edge.From("groups", Group.Type).
Ref("users"),
}
}
修改后,重新生成代码:
go generate ./ent
在start.go
中调用AddUserToGroup
方法,输出示例如下:
2023/05/01 21:08:40 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user1]
2023/05/01 21:08:40 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user2]
2023/05/01 21:08:40 driver.Tx(ac478d40-22eb-40ad-8324-a0ca461e5dd8): started
2023/05/01 21:08:40 Tx(ac478d40-22eb-40ad-8324-a0ca461e5dd8).Query: query=INSERT INTO `groups` (`name`) VALUES (?) RETURNING `id` args=[group1]
2023/05/01 21:08:40 Tx(ac478d40-22eb-40ad-8324-a0ca461e5dd8).Exec: query=INSERT INTO `group_users` (`group_id`, `user_id`) VALUES (?, ?), (?, ?) ON CONFLICT DO NOTHING args=[1 1 1 2]
2023/05/01 21:08:40 Tx(ac478d40-22eb-40ad-8324-a0ca461e5dd8): committed
根据日志可以发现,ent
创建了group_users
关联表来保存User
和Group
的关联关系。
关联查询
在对应的模型的QueryBuilder(client.<Model>.Query()
) 下,可以构建关联模型的QueryBuilder,进而完成关联查询,例如在下面的示例中 ,在client.User.Query()
下再调用QueryCars()
即可得到Car
的QueryBuilder:
func QueryUserCars(client *ent.Client) {
// 初始化测试数据,创建用户和车
u := client.User.Create().SetName("user1").SaveX(context.Background())
client.Car.Create().SetName("car1").SetOwner(u).SaveX(context.Background())
// 以下两种方法都可
//car := client.Debug().User.QueryCars(u).AllX(context.Background())
car := client.Debug().User.Query().Where(user.Name("user1")).QueryCars().AllX(context.Background())
fmt.Println(car)
}
上述方法执行输出如下:
2023/05/01 21:34:48 driver.Query: query=SELECT DISTINCT `cars`.`id`, `cars`.`name` FROM `cars` JOIN (SELECT `users`.`id` FROM `users` WHERE `users`.`name` = ?) AS `t1` ON `cars`.`user_cars` = `t1`.`id` args=[user1]
[Car(id=1, name=car1)]
即时加载(eager-loading)
通过在Query()
中使用WithXXX()
方法即可实现即时加载。
func QueryUserWithCarsEagerLoading(client *ent.Client) {
// 初始化测试数据,创建用户和车
u := client.User.Create().SetName("user1").SaveX(context.Background())
client.Car.Create().SetName("car1").SetOwner(u).SaveX(context.Background())
// 使用WithXXX()方法实现即时加载
u = client.Debug().User.Query().WithCars().OnlyX(context.Background())
fmt.Println(u.Edges.CarsOrErr())
}
上述方法执行输出如下:
2023/05/01 21:31:05 driver.Query: query=SELECT `users`.`id`, `users`.`name` FROM `users` LIMIT 2 args=[]
2023/05/01 21:31:05 driver.Query: query=SELECT `cars`.`id`, `cars`.`name`, `cars`.`user_cars` FROM `cars` WHERE `cars`.`user_cars` IN (?) args=[1]
[Car(id=1, name=car1)] <nil>
自定义关联字段/关联表
根据前面的示例,我们发现,ent
在表达关联关系时,为我们创建了user_cars
关联字段和group_users
关联表。
在某些场景场景下(特别是数据库已存在情况下),我们希望能控制关联字段和关联表的名称。在ent
中,关联关系由edge.To
来定义,我们可以通过StorageKey
方法来配置edge.To
。
例如,在Car
表中使用user_id
来表示车的所有者,(而不是默认的user_cars
字段):
// entdemo/ent/schema/user.go
func (User) Edges() []ent.Edge {
return []ent.Edge{
// 定义一条名为"cars"的,指向Car类型的边
edge.To("cars", Car.Type).StorageKey(edge.Column("user_id")),
// 创建一个指向Group类型的反向关联关系”groups“。
// 通过Ref方法,显示的将其与在Group中定义的“users”关联关系关联。
edge.From("groups", Group.Type).
Ref("users"),
}
}
使用group_users_binding
表来保存User
和Group
的关联关系(而不是group_user
):
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
// 定义一条名为"users",指向User类型的边
edge.To("users", User.Type).StorageKey(
edge.Table("group_users_binding"),
edge.Columns("user_id", "group_id"),
),
}
}
使用Mixin复用公共逻辑
相关文档: entgo.io/docs/schema…
我们在设计数据库表时,可能会定义一些公共字段,例如在每个表中使用create_time
, update_time
字段来表达创建和更新时间,使用delete_time
来表达软删除以及其删除时间,使用user_id
来表达归属用户等等。
在每个schema中去重复定义这些字段显然是不可取的,ent
提供Mixin的功能,允许我们定义可重用的部分,通过组合的方式注入到需要的模型中。我们以使用delete_time
做软删除为例:
创建entdemo/ent/schema/softdelemixin.go
文件:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
)
// SoftDeleteMixin implements the soft delete pattern for schemas.
type SoftDeleteMixin struct {
mixin.Schema
}
// Fields of the SoftDeleteMixin.
func (SoftDeleteMixin) Fields() []ent.Field {
return []ent.Field{
field.Time("delete_time").Optional(),
}
}
修改entdemo/ent/schema/{car,group,user}.go
三个文件,通过增加Mixin()
方法引入SoftDeleteMixin
:
// car.go
// Car holds the schema definition for the Car entity.
type Car struct {
ent.Schema
}
func (Car) Mixin() []ent.Mixin {
return []ent.Mixin{
SoftDeleteMixin{},
}
}
// user.go
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
func (User) Mixin() []ent.Mixin {
return []ent.Mixin{
SoftDeleteMixin{},
}
}
// group.go
// Group holds the schema definition for the Group entity.
type Group struct {
ent.Schema
}
func (Group) Mixin() []ent.Mixin {
return []ent.Mixin{
SoftDeleteMixin{},
}
}
修改后,重新生成代码:
go generate ./ent
运行下面的main函数,打印建表语句:
func main() {
client, err := ent.Open(
"sqlite3",
"file:ent?mode=memory&cache=shared&_fk=1",
+ // 添加ent.Debug(),打印建表语句
+ ent.Debug(),
)
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
// client.Schema.Create 会自动建表,示例中使用内存模式的数据库,所以每次都会重新建表。
if err := client.Schema.Create(
context.Background(),
// ent默认会为关联字段创建外键,按使用习惯决定是否增加这行配置禁用外键。
migrate.WithForeignKeys(false),
); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
建表语句输出示例:
...
2023/05/02 16:54:22 Tx(654a3ff8-f9b1-4d2a-bfa1-d3d2df1789b5).Exec: query=CREATE TABLE `cars` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `delete_time` datetime NULL, `name` text NOT NULL, `user_id` integer NULL) args=[]
2023/05/02 16:54:22 Tx(654a3ff8-f9b1-4d2a-bfa1-d3d2df1789b5).Exec: query=CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `delete_time` datetime NULL, `name` text NOT NULL) args=[]
2023/05/02 16:54:22 Tx(654a3ff8-f9b1-4d2a-bfa1-d3d2df1789b5).Exec: query=CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `delete_time` datetime NULL, `name` text NOT NULL) args=[]
....
可发现delete_time
字段已经添加到数据库表结构中。
不过要真正实现软删除功能——即在调用删除方法时执行软删除,在查询时排除已删除的数据,还需要借助接下来将介绍的ent
钩子和拦截器功能来实现。
Hook(钩子)与 Interceptor(拦截器)
ent
提供了Hook(钩子)与Intercepter(拦截器)方便开发者可以在数据库变更前后和数据库查询前后插入自定义逻辑。
基于钩子和拦截器,开发者可以扩展相当丰富的能力,例如集成链路跟踪,metric性能指标,软删除,慢查询记录等等丰富等能力。
Hook
参考文档: entgo.io/docs/hooks
Hook
机制针对数据库变更操作,ent
定义了五种变更类型:
Create
- 创建操作UpdateOne
- 更新单条数据Update
- 更新多条数据DeleteOne
- 删除一条数据Delete
- 删除多条数据
开发者可以根据需要,在不同的变更类型上注册钩子函数,自定义逻辑。如下例所示,通过client.Use
注册全局钩子,打印数据库语句的执行耗时:
func main() {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run the auto migration tool.
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Add a global hook that runs on all types and all operations.
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
start := time.Now()
defer func() {
log.Printf("Op=%s\tType=%s\tTime=%s\tConcreteType=%T\n", m.Op(), m.Type(), time.Since(start), m)
}()
return next.Mutate(ctx, m)
})
})
client.User.Create().SetName("a8m").SaveX(ctx)
// Output:
// 2020/03/21 10:59:10 Op=Create Type=User Time=46.23µs ConcreteType=*ent.UserMutation
}
全局钩子可以方便用户增加traces,metrics,logs等信息。同时,如果用户需要更细粒度的控制,也可以针对某个模型,或者是某些变更类型来注册钩子:
func main() {
// <client was defined in the previous block>
// 这个Hook只对User的变更生效
client.User.Use(func(next ent.Mutator) ent.Mutator {
// Use the "<project>/ent/hook" to get the concrete type of the mutation.
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
return next.Mutate(ctx, m)
})
})
// Logger()只对更新操作生效
client.Use(hook.On(Logger(), ent.OpUpdate|ent.OpUpdateOne))
// 不允许所有的删除操作
client.Use(hook.Reject(ent.OpDelete|ent.OpDeleteOne))
}
在Schema中定义Hook
除了在运行时代码显示绑定Hook外,Hook还可以和具体的schema绑定,示例如下:
// entdemo/ent/schema/car.go
// Car holds the schema definition for the Car entity.
type Car struct {
ent.Schema
}
// Hooks 定义的钩子只对Car生效
func (c Car) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
return next.Mutate(ctx, m)
})
},
ent.OpUpdate|ent.OpUpdateOne,
),
}
}
需要注意到是,当使用schema hook的时候,需要import <project>/ent/runtime
来避免循环引用问题,在本教程中,即import entdemo/ent/runtime
,在start.go
中添加import:
// entdemo/start.go
import _ "entdemo/ent/runtime"
Interceptor
与Hook
的针对修改操作相对应,拦截器针对数据库的查询操作。
在使用拦截器之前,我们需要修改代码生成配置,修改ent/generate.go
文件,增加 --feature intercept,schema/snapshot
配置:
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept,schema/snapshot ./schema
修改完后,执行go generate ./ent
生成拦截器代码。
要定义拦截器,用户可以声明一个实现Intercept
方法的结构体,或者使用ent.InterceptFunc
帮助方法:
// 定义拦截器
inter := ent.InterceptFunc(func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
// 查询前做一些操作
value, err := next.Query(ctx, query)
// 查询后做一些操作
return value, err
})
})
// 使用拦截器
client.Intercept(inter)
示例:使用钩子和拦截器实现软删除
接下来我们以软删除为例,来进一步说明钩子和拦截器的使用方法。
修改entdemo/ent/shcema/softdeletemixin.go
文件,完整的代码如下:
package schema
import (
"context"
"fmt"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
gen "entdemo/ent"
"entdemo/ent/hook"
"entdemo/ent/intercept"
)
// SoftDeleteMixin implements the soft delete pattern for schemas.
type SoftDeleteMixin struct {
mixin.Schema
}
// Fields of the SoftDeleteMixin.
func (SoftDeleteMixin) Fields() []ent.Field {
return []ent.Field{
field.Time("delete_time").Optional(),
}
}
type softDeleteKey struct{}
// SkipSoftDelete 返回一个context,表示在修改和查询阶段跳过软删除逻辑。
func SkipSoftDelete(parent context.Context) context.Context {
return context.WithValue(parent, softDeleteKey{}, true)
}
// Interceptors 定义拦截器
func (d SoftDeleteMixin) Interceptors() []ent.Interceptor {
return []ent.Interceptor{
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
// 跳过软删除,查询也会将被删除的数据查出来
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
return nil
}
d.P(q)
return nil
}),
}
}
// Hooks 定义钩子。通过Hooks()方法定义的钩子,与当前schema/mixin绑定。
func (d SoftDeleteMixin) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// 跳过软删除,直接删除
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
return next.Mutate(ctx, m)
}
mx, ok := m.(interface {
SetOp(ent.Op)
Client() *gen.Client
SetDeleteTime(time.Time)
WhereP(...func(*sql.Selector))
})
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m)
}
d.P(mx)
// 删除操作改成更新操作
mx.SetOp(ent.OpUpdate)
// 设置当前删除时间
mx.SetDeleteTime(time.Now())
return mx.Client().Mutate(ctx, m)
})
},
ent.OpDeleteOne|ent.OpDelete,
),
}
}
// P 方法增加另一个更强的筛选校验,只删除/查询delete_time is null的数据。
func (d SoftDeleteMixin) P(w interface{ WhereP(...func(*sql.Selector)) }) {
w.WhereP(
sql.FieldIsNull(d.Fields()[0].Descriptor().Name),
)
}
重新生成代码go generate ./ent
,再次执行CreateAndQueryUser
方法,输出如下:
2023/05/02 15:24:36 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user1]
2023/05/02 15:24:36 driver.Query: query=SELECT `users`.`id`, `users`.`delete_time`, `users`.`name` FROM `users` WHERE `users`.`name` = ? AND `users`.`delete_time` IS NULL args=[user1]
[User(id=1, delete_time=Mon Jan 1 00:00:00 0001, name=user1)] <nil>
观察日志得知,ent
使用了"users.delete_time IS NULL"来排查已被删除的用户。
如果我们使用Delete()
来删除用户:
func DeleteUser(client *ent.Client) {
// 初始化测试数据
client.User.Create().SetName("user1").SaveX(context.Background())
client.Debug().User.Delete().Where(user.Name("user1")).ExecX(context.Background())
}
ent
会将原本需要执行的delete方法转变成update,更新delete_time
字段的值:
2023/05/02 15:33:56 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user1]
2023/05/02 15:33:56 driver.Exec: query=UPDATE `users` SET `delete_time` = ? WHERE `users`.`name` = ? AND `users`.`delete_time` IS NULL args=[2023-05-02 15:33:56.616235 +0800 CST m=+0.019237918 user1]
如果我们的确需要删除数据库记录时,可通过context传递给钩子/拦截器:
func DeleteUserForceDelete(client *ent.Client) {
// 初始化测试数据
client.User.Create().SetName("user1").SaveX(context.Background())
ctx := context.Background()
// 跳过软删除,直接删除数据库记录
ctx = schema.SkipSoftDelete(ctx)
client.Debug().User.Delete().Where(user.Name("user1")).ExecX(ctx)
}
输出示例如下:
2023/05/02 15:34:20 driver.Query: query=INSERT INTO `users` (`name`) VALUES (?) RETURNING `id` args=[user1]
2023/05/02 15:34:20 driver.Exec: query=DELETE FROM `users` WHERE `users`.`name` = ? args=[user1]
观察日志得知,ent
执行了数据库delete语句。
使用事务
事务的使用示例如下:
// entdemo/start.go
...
func GenTx(ctx context.Context, client *ent.Client) error {
client = client.Debug()
// 开启事务
tx, err := client.Tx(ctx)
if err != nil {
return fmt.Errorf("starting a transaction: %w", err)
}
_, err = tx.User.Create().SetName("user1").Save(ctx)
if err != nil {
// 失败回滚
return rollback(tx, err)
}
_, err = tx.Car.Create().SetName("car1").Save(ctx)
if err != nil {
return rollback(tx, err)
}
// 提交事务
return tx.Commit()
}
// rollback 事务回滚方法
func rollback(tx *ent.Tx, err error) error {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: %v", err, rerr)
}
return err
}
client.Tx()
方法将开启事务,并返回事务client *ent.Tx
,*ent.Tx
提供提交,回滚等操作。
同时,ent
的文档还贴心的为我们提供了事务使用的最佳实践指引:entgo.io/docs/transa…
通过定义一个WithTx
方法,将事务的开启,提交回滚封装起来:
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
// 开启事务
tx, err := client.Tx(ctx)
if err != nil {
return err
}
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
}()
// 错误回滚
if err := fn(tx); err != nil {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr)
}
return err
}
// 成功提交
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
return nil
}
通过WithTx
方法,我们可以实现对已有的非事务方法进行复用,示例代码如下所示:
WithTx(context.Background(), client.Debug(), func(tx *ent.Tx) error {
// tx.Client() 返回 *ent.Client,实现复用已有非事务方法。
CreateAndQueryUser(tx.Client())
AddCarsToUser(tx.Client())
return nil
})
结语
ent
虽然还迟迟没有发布v1版本(吐槽一下v1的Roadmap从2019年10持续到现在),但其作为ORM框架的整体功能已经十分完备。从最近学习使用的感受上来看,其API定义简洁清晰,并且有不错的扩展能力,文档也相对完善,体验相当不错,非常推荐大家尝试。
转载自:https://juejin.cn/post/7228521515507253304