ent ORM框架入门教程

lxf2023-05-03 00:35:16

前言

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

在本教程中,将使用UserGroupCar (用户,用户组,车)三个模型为示例。

使用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有以下关联关系:

ent ORM框架入门教程

即:

  • 用户可以拥有多辆汽车,但一辆汽车只能属于一个用户。用户和车为一对多(One-to-Many)关系。
  • 用户组可以包含多个用户,同时一个用户可以加入多个用户组。用户和用户组为多对多(Many-to-Many)关系。

ent使用图的概念来表达数据之间的关联模型,在ent中,关联关系使用edge(边)来定义。

一对多关系

ent ORM框架入门教程

修改user.goEdges()方法,增加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]

根据日志可以发现,entcars表中定义了user_cars字段来表达关联关系。

多对多关系

ent ORM框架入门教程

接下来我们创建UserGroup的多对多关系。

分别修改entdemo/ent/schema/user.goentdemo/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关联表来保存UserGroup的关联关系。

关联查询

在对应的模型的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>

自定义关联字段/关联表

相关文档:
entgo.io/docs/schema… entgo.io/docs/schema…

根据前面的示例,我们发现,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表来保存UserGroup的关联关系(而不是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

参考文档:entgo.io/docs/interc…

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定义简洁清晰,并且有不错的扩展能力,文档也相对完善,体验相当不错,非常推荐大家尝试。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!