懒人必备!entorm教程来了!

懒人必备!Ent ORM 教程

关键词:类、模板、ORM、Ent、GORM、Schema as Code、代码生成、CRUD、Edge、事务、context


引言

相信大家都对“类”和“模板”这些概念不陌生吧!程序员们通过定义好的类或模板,直接生成对应的代码,不仅节省工作量,还大大提升了代码的可读性。

在重构或修改代码时,只需调整类或模板即可,就像工程师根据蓝图建造房子——修改蓝图总比拆房子重盖方便多了。

那么,数据库 ORM(Object-Relational Mapping)框架能不能也这么玩呢?Ent ORM 应运而生!Ent ORM 是一种基于对象的数据库映射框架,类似于 GORM,但它更注重以对象为中心进行操作,并且会自动生成代码,超级方便。

Ent ORM 的核心理念是 Schema as Code(模板即代码)。你只需专注编写 Schema 模板,剩下的 CRUD 方法和迁移逻辑 Ent 会自动生成,避免了运行时反射,类型更安全,效率更高。

AI 训话:Ent 通过生成器自动创建 CRUD 方法和迁移。这避免了运行时反射(GORM 用反射扫描 struct tag),更类型安全。编译时检查错误,让代码变更自动同步到数据库。


快速上手:创建第一个 Schema

按照 Ent 的操作流程:先创建模板 Schema,然后生成对象,再对对象进行操作

1. 创建 Schema 模板

首先,创建 ent/schema/ 文件夹用于存放 Schema 模板代码。

引入必要包:

1
2
3
4
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

定义 Schema:

1
2
3
4
5
6
7
8
9
10
type Men struct {
ent.Schema // 嵌入基类,建立代码关联
}

func (Men) Fields() []ent.Field {
return []ent.Field{
field.String("name"), // 名字属性
field.Int("age"), // 年龄属性
}
}

代码逐行解释

1
2
3
type Men struct {
ent.Schema
}

赋予结构体 ent.Schema 的属性,告诉 Ent:“这是一个 Schema 模板”。

1
2
3
4
5
6
func (Men) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age"),
}
}

通过 Fields() 方法赋予模板属性。[]ent.Field 就是“字段总和”。

2. 字段选项(Tag)

Ent 使用链式调用(Builder Pattern)来添加选项,类似于方法调用,非常优雅。

常见选项:

  • .Default(18):设置默认值
  • .Optional():允许 NULL 值(默认非 NULL)
  • .Unique():添加 UNIQUE 约束
  • .NotEmpty():字符串非空
  • .Min(0) / .Max(100) / .Positive() 等:范围/验证
  • .Comment("用户年龄"):添加数据库注释
  • .StorageKey("custom_age"):自定义数据库列名
  • .StructTag(json:"age,omitempty"):添加 Go struct 的 tag

示例:

1
2
3
field.String("name").
Comment("名字").
Optional()

代码生成与数据库连接

1. 代码生成

运行命令:

1
go generate ./ent

这会根据 Schema 生成 CRUD 方法和相关代码。

2. 连接数据库

GORM 示例:

1
2
3
4
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

Ent ORM 示例:

1
2
3
4
5
6
7
8
func main() {
ctx := context.Background()
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test?parseTime=True")
if err != nil {
log.Fatal(err)
}
defer client.Close()
}

自动迁移

1
2
3
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("迁移失败: %v", err)
}

增删改查(CRUD)

增(Create)

1
2
3
4
5
6
7
8
men, err := client.Men.
Create().
SetName("奶农").
SetAge(222).
Save(ctx)
if err != nil {
log.Fatal("奶农创建失败")
}

删(Delete)

1
2
3
4
5
6
7
err := client.Men.
Delete().
Where(men.ID(1)).
Exec(ctx)
if err != nil {
// 处理错误
}

改(Update)

1
2
3
4
5
6
7
8
_, err := client.Men.
Update().
Where(men.ID(1)).
SetAge(123).
Save(ctx)
if err != nil {
// 处理错误
}

查(Query)

1
2
3
4
p, _ := client.Men.
Query().
Where(men.ID(1)).
Only(ctx) // 类比 GORM 的 First,返回单个对象;要所有对象就用 All(ctx)

关键词:SetName、SetAge、Only、All、CRUD、代码生成


进阶:模板之间的关联(外键/关系)

1. 一对多(O2M)

User 与 Pet 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ent/schema/user.go
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)


ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type). // User -> Pets (O2M)
Required().
Comment("用户拥有的宠物"),
}
}

// ent/schema/pet.go
type Pet struct {
ent.Schema
}

func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique(),
}
}

关键词:Edge、edge.To、edge.From、Ref、Unique、O2M


多对多(M2M)关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ent/schema/group.go
type Group struct {
ent.Schema
}

func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type), // Group -> Users (M2M)
}
}

// ent/schema/user.go
type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("groups", Group.Type).
Ref("users"),
}
}

关系操作方法

常用方法:

  • 添加关系:AddUsers()AddGroups()
  • 查询关系:QueryUsers()QueryGroups()
  • 更新关系:Update().AddGroups()RemoveGroups()ClearGroups()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "context"  // 假设 ctx = context.Background()

// 创建并链接
hub, _ := client.Group.Create().SetName("GitHub").Save(ctx)
a8m, _ := client.User.Create().SetName("a8m").AddGroups(hub).Save(ctx) // 添加关系

// 查询用户的组
groups, _ := a8m.QueryGroups().All(ctx)

// 遍历关系(链式查询)
users, _ := a8m.QueryGroups().
Where(group.Not(group.HasUsersWith(user.Name("nati")))).
QueryUsers().
All(ctx)

// 双向朋友关系(自动互加)
nati, _ := client.User.Create().SetName("nati").AddFriends(a8m).Save(ctx)

高级用法:带属性的多对多(中间表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ent/schema/friendship.go
type Friendship struct {
ent.Schema
}

func (Friendship) Fields() []ent.Field {
return []ent.Field{
field.Time("since").Default(time.Now),
}
}

// 在 user.go 的 Edges 中
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("friends", User.Type).
Through("friendships", Friendship.Type),
}
}

事务支持(原子性操作)

场景: 银行转账,保证扣款和加款要么都成功,要么都失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tx, err := client.Tx(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
if err := tx.Commit(); err != nil {
log.Printf("提交失败: %v", err)
}
}()

// 操作1: 扣款
nainong, _ := tx.User.Query().Where(user.Name("Nainong")).Only(ctx)
if nainong.Balance < 50 {
panic("余额不足")
}
nainong.Update().SetBalance(nainong.Balance - 50).Save(ctx)

// 操作2: 加款
bangbang, _ := tx.User.Query().Where(user.Name("BangBang")).Only(ctx)
bangbang.Update().SetBalance(bangbang.Balance + 50).Save(ctx)

关键词:事务、Tx、Rollback、Commit、原子性、ACID


上下文(context.Context)

Go 鼓励显式传递 ctx,便于取消、超时控制,避免阻塞。


总结

Ent 实现了优雅的 ORM,面向对象操作,借助 Field 和 Edge 表现数据库联系,优雅直观。优势:避免 GORM 的反射,类型安全,代码生成高效。

推荐阅读Ent 官方文档
引入
相信大家都对“类”和“模板”这些概念不陌生吧!程序员们通过定义好的类或模板,直接生成对应的代码,不仅节省工作量,还大大提升了代码的可读性。这样写代码,是不是超级无敌好用?在重构或修改代码时,只需调整类或模板即可,就像工程师根据蓝图建造房子——修改蓝图总比拆房子重盖方便多了(虽然现实中不太可能)。
那么,数据库 ORM(Object-Relational Mapping)框架能不能也这么玩呢?Ent ORM 应运而生!Ent ORM 是一种基于对象的数据库映射框架,类似于 GORM,但它更注重以对象为中心进行操作(不过 GORM 也是对象式的,但 Ent 更贴近面向对象编程范式,最重要的是它会自动生成代码,超级方便)。
就像 Go 语言的哲学:“通过通信共享内存,而不是通过共享内存通信”,Ent ORM 也有自己的核心理念——“Schema as Code”,翻译成“模板即代码”。为什么这么说?因为 Ent 不需要你手动编写底层 SQL 或复杂的配置,而是通过定义模板(Schema)来描述数据库结构和操作。Ent 的生成器会自动创建 CRUD 方法和迁移逻辑,这避免了运行时反射(GORM 使用反射扫描 struct tag),从而更类型安全、更高效。我们只需专注编写 Schema 模板,剩下的交给 Ent 处理。
AI 训话:Ent 通过生成器自动创建 CRUD 方法和迁移。这避免了运行时反射(GORM 用反射扫描 struct tag),更类型安全。编译时检查错误,让代码变更自动同步到数据库。
小试牛刀
直接讲解可能不大好理解,所以我们直接上手写代码。按照 Ent 的操作流程:先创建模板 Schema,然后生成对象,再对对象进行操作(很像面向对象的语言)。
创建模板 Schema
首先,我们创建一个 ent/schema/ 文件夹,用来存放 Schema 模板代码。下面的编写就在这里面。
为了方便理解,先引入必要的包(后面例子中会省略,但实际代码中必须有):
Go
import (
“entgo.io/ent”
“entgo.io/ent/schema/field”
)
现在,直接上代码:
Go
type Men struct {
ent.Schema // 嵌入基类,建立代码关联
}

func (Men) Fields() []ent.Field {
return []ent.Field{
field.String(“name”), // 加上名字属性
field.Int(“age”), // 加上年龄属性
}
}
这就是一个最简单的 Schema 定义。在这段代码中,我们定义了一个名为 Men 的模板,它拥有“名字”和“年龄”两个属性。
接下来,我们逐行解释代码及其作用。
首先:
Go
type Men struct {
ent.Schema
}
这赋予了结构体 ent.Schema 的属性,意思就是告诉 Ent:“这是一个 Schema 模板”,Ent 的生成工具就可以据此操作它。打个比方,就像告诉编译器:“我是模板,快来生成我的代码吧!”
然后:
Go
func (Men) Fields() []ent.Field {
return []ent.Field{
field.String(“name”), // 加上名字属性
field.Int(“age”), // 加上年龄属性
}
}
这一部分就是模板的定义过程。我们通过 Fields() 方法赋予模板属性。以什么样的方式呢?没错,就是代码里的 field 函数,通过给模板添加对应的字段(Field),实现对数据库的操作。这样你能理解 []ent.Field 吗?它就是“字段总和”的意思,一个切片收集所有字段。
那我们再添加点别的。我们知道 GORM 中的 tag 可以指定外键、主键、索引、默认值或非空等 SQL 属性,那么 Ent 的 Schema 有吗?
Ent ORM 的 Tag(选项)
有的!Ent 使用链式调用(Builder Pattern)来添加选项,类似于方法调用,非常优雅。以下是常见选项列表:
● 默认值:.Default(18) – 设置默认值。
● 可选:.Optional() – 允许 NULL 值(默认是非 NULL)。
● 唯一:.Unique() – 添加 UNIQUE 约束。
● 非空:.NotEmpty() – 对于字符串,确保不为空字符串(但允许 NULL,除非结合 Optional)。
● 范围/验证:.Min(0) / .Max(100) / .Positive() / .Negative() / .NonNegative() – 对于数字字段的范围检查(编译时或运行时)。
● 注释:.Comment(“用户年龄”) – 添加数据库注释(SQL COMMENT)。
● 存储名:.StorageKey(“custom_age”) – 自定义数据库列名(默认用字段名)。
● Struct Tag:.StructTag(json:”age,omitempty”) – 添加 Go struct 的 tag(如 JSON 序列化)。
● 敏感:.Sensitive() – 标记为敏感数据(如密码),在日志/调试中隐藏。
● 不可变:.Immutable() – 字段创建后不可更新。
● Nillable:.Nillable() – 对于值类型(如 int),允许 nil(转为 NULL)。
● 其他:对于字符串,还有 .MaxLen(255) / .MinLen(1);对于时间,还有 .Immutable() 等。完整列表见 Ent 文档的 field 包。
一般来说,常用的是 Default、Optional 和 Comment。用法就是在字段后打点链式调用:
Go
field.String(“name”).
Comment(“名字”).
Optional(), // 如果是最后一个,不用逗号;否则加逗号

引言

相信大家都对“类”和“模板”这些概念不陌生吧!程序员们通过定义好的类或模板,直接生成对应的代码,不仅节省工作量,还大大提升了代码的可读性。

关键词:类、模板、ORM、Ent、GORM、Schema as Code、代码生成

在重构或修改代码时,只需调整类或模板即可,就像工程师根据蓝图建造房子——修改蓝图总比拆房子重盖方便多了。

那么,数据库 ORM(Object-Relational Mapping)框架能不能也这么玩呢?Ent ORM 应运而生!Ent ORM 是一种基于对象的数据库映射框架,类似于 GORM,但它更注重以对象为中心进行操作,并且会自动生成代码,超级方便。

Ent ORM 的核心理念是 Schema as Code(模板即代码)。你只需专注编写 Schema 模板,剩下的 CRUD 方法和迁移逻辑 Ent 会自动生成,避免了运行时反射,类型更安全,效率更高。

AI 训话:Ent 通过生成器自动创建 CRUD 方法和迁移。这避免了运行时反射(GORM 用反射扫描 struct tag),更类型安全。编译时检查错误,让代码变更自动同步到数据库。


快速上手:创建第一个 Schema

按照 Ent 的操作流程:先创建模板 Schema,然后生成对象,再对对象进行操作

1. 创建 Schema 模板

首先,创建 ent/schema/ 文件夹用于存放 Schema 模板代码。

引入必要包:

1
2
3
4
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

定义 Schema:

1
2
3
4
5
6
7
8
9
10
type Men struct {
ent.Schema // 嵌入基类,建立代码关联
}

func (Men) Fields() []ent.Field {
return []ent.Field{
field.String("name"), // 名字属性
field.Int("age"), // 年龄属性
}
}

这就是一个最简单的 Schema 定义。我们定义了一个名为 Men 的模板,它拥有“名字”和“年龄”两个属性。

代码逐行解释

1
2
3
type Men struct {
ent.Schema
}

赋予结构体 ent.Schema 的属性,告诉 Ent:“这是一个 Schema 模板”。

1
2
3
4
5
6
func (Men) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age"),
}
}

通过 Fields() 方法赋予模板属性。[]ent.Field 就是“字段总和”。

2. 字段选项(Tag)

Ent 使用链式调用(Builder Pattern)来添加选项,类似于方法调用,非常优雅。

常见选项:

  • .Default(18):设置默认值
  • .Optional():允许 NULL 值(默认非 NULL)
  • .Unique():添加 UNIQUE 约束
  • .NotEmpty():字符串非空
  • .Min(0) / .Max(100) / .Positive() 等:范围/验证
  • .Comment("用户年龄"):添加数据库注释
  • .StorageKey("custom_age"):自定义数据库列名
  • .StructTag(json:"age,omitempty"):添加 Go struct 的 tag

示例:

1
2
3
field.String("name").
Comment("名字").
Optional()

代码生成与数据库连接

1. 代码生成

运行命令:

1
go generate ./ent

这会根据 Schema 生成 CRUD 方法和相关代码。

2. 连接数据库

GORM 示例:

1
2
3
4
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

Ent ORM 示例:

1
2
3
4
5
6
7
8
func main() {
ctx := context.Background()
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test?parseTime=True")
if err != nil {
log.Fatal(err)
}
defer client.Close()
}

自动迁移

1
2
3
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("迁移失败: %v", err)
}

增删改查(CRUD)

增(Create)

1
2
3
4
5
6
7
8
men, err := client.Men.
Create().
SetName("奶农").
SetAge(222).
Save(ctx)
if err != nil {
log.Fatal("奶农创建失败")
}

删(Delete)

1
2
3
4
5
6
7
err := client.Men.
Delete().
Where(men.ID(1)).
Exec(ctx)
if err != nil {
// 处理错误
}

改(Update)

1
2
3
4
5
6
7
8
_, err := client.Men.
Update().
Where(men.ID(1)).
SetAge(123).
Save(ctx)
if err != nil {
// 处理错误
}

查(Query)

1
2
3
4
p, _ := client.Men.
Query().
Where(men.ID(1)).
Only(ctx) // 类比 GORM 的 First,返回单个对象;要所有对象就用 All(ctx)

关键词:SetName、SetAge、Only、All、CRUD、代码生成


进阶:模板之间的关联(外键/关系)

1. 一对多(O2M)

User 与 Pet 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ent/schema/user.go
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)

type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type). // User -> Pets (O2M)
Required().
Comment("用户拥有的宠物"),
}
}

// ent/schema/pet.go
type Pet struct {
ent.Schema
}

func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique(),
}
}

关键词:Edge、edge.To、edge.From、Ref、Unique、O2M


多对多(M2M)关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ent/schema/group.go
type Group struct {
ent.Schema
}

func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type), // Group -> Users (M2M)
}
}

// ent/schema/user.go
type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("groups", Group.Type).
Ref("users"),
}
}

关系操作方法

常用方法:

  • 添加关系:AddUsers()AddGroups()
  • 查询关系:QueryUsers()QueryGroups()
  • 更新关系:Update().AddGroups()RemoveGroups()ClearGroups()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建并链接
hub, _ := client.Group.Create().SetName("GitHub").Save(ctx)
Where(men.ID(1)).
SetAge(123).
Save(ctx)
groups, _ := a8m.QueryGroups().All(ctx)

// 遍历关系(链式查询)
users, _ := a8m.QueryGroups().
Where(group.Not(group.HasUsersWith(user.Name("nati")))).
QueryUsers().
All(ctx)

// 双向朋友关系(自动互加)
nati, _ := client.User.Create().SetName("nati").AddFriends(a8m).Save(ctx)

高级用法:带属性的多对多(中间表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ent/schema/friendship.go
type Friendship struct {
ent.Schema
}

func (Friendship) Fields() []ent.Field {
return []ent.Field{
field.Time("since").Default(time.Now),
}
}

// 在 user.go 的 Edges 中
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("friends", User.Type).
Through("friendships", Friendship.Type),
}
}

事务支持(原子性操作)

场景: 银行转账,保证扣款和加款要么都成功,要么都失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tx, err := client.Tx(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
if err := tx.Commit(); err != nil {
log.Printf("提交失败: %v", err)
}
}()

// 操作1: 扣款
nainong, _ := tx.User.Query().Where(user.Name("Nainong")).Only(ctx)
if nainong.Balance < 50 {
panic("余额不足")
}
if err != nil {

// 操作2: 加款
bangbang, _ := tx.User.Query().Where(user.Name("BangBang")).Only(ctx)
bangbang.Update().SetBalance(bangbang.Balance + 50).Save(ctx)

关键词:事务、Tx、Rollback、Commit、原子性、ACID


上下文(context.Context)

Go 鼓励显式传递 ctx,便于取消、超时控制,避免阻塞。


总结

Ent 实现了优雅的 ORM,面向对象操作,借助 Field 和 Edge 表现数据库联系,优雅直观。优势:避免 GORM 的反射,类型安全,代码生成高效。

推荐阅读Ent 官方文档
// 处理错误
}
查:
Go
p, _ := client.Men.
Query().
Where(men.ID(1)).
Only(ctx) // 类比 GORM 的 First,返回单个对象;要所有对象就用 All(ctx)
看了这些,你大概对 Ent 以对象为基础的操作有一定了解。每条语句都有“主语 + 谓语 + 宾语”的结构,可以归纳使用方法。
PS:初学者可能会疑惑为什么能直接用 SetName 等(我一开始也这样),“我明明没定义这个函数,怎么能用呢?”这是典型的 C++ 思维。Go 支持代码生成:Ent 根据 Schema Fields 自动生成这些方法(如 SetName 是基于 “name” 字段生成的)。类似于 Go 测试的前缀(TestFunc 或 BenchmarkFunc),Ent 用前缀如 Set + 字段名动态生成。
进阶玩法:模板之间的关联(外键)
如果我们刚刚定义的是孤立的对象,那怎么实现对象间的联系呢?
为了帮助初学者理解,想象对象就是一个个图形。我们现实中用连线把图形链接起来;Ent 也这样,用 “Edge” 把模板联系起来。
Edge 相当于带箭头的直线,表示从属关系。使用 edge.To(name, target.Type) 定义拥有方(Owning Edge),如 edge.To(“pets”, Pet.Type)。使用 edge.From(name, source.Type).Ref(“owning_edge_name”) 定义反向引用。选项链式调用:如 .Unique()(唯一性)、.Required()(非空)、.StorageKey(edge.Column(“custom_id”))(自定义列名)。
直接举例:主人和宠物。一个主人可以有多个宠物,一个宠物只有一个主人(就像你喜欢一个人,但她不喜欢你 QWQ)。
Go
// ent/schema/user.go
import (
“entgo.io/ent”
“entgo.io/ent/schema/edge”
“entgo.io/ent/schema/field”
)

type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String(“name”),
}
}

func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To(“pets”, Pet.Type). // Owning: User -> Pets (O2M)
Required(). // 强制非空(从 v0.10+)
Comment(“用户拥有的宠物”), // 添加注释
}
}

// ent/schema/pet.go
type Pet struct {
ent.Schema
}

func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String(“name”),
}
}

func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From(“owner”, User.Type).
Ref(“pets”). // 引用 User 的 “pets” 边
Unique(), // M2O(多个 Pet 指向同一 User,但每个 Pet 唯一 Owner)
}
}
Fields 部分就不解释了,直接讲 Edge。就像刚刚说的,Ent 定义 Schema 然后赋予 Fields,这个模板的关系算不算属性呢?答案是算的。
和 Fields 类似,Edge 的定义也是基于对象的:edge.To 表示“我是谁的主人”(外键在被拥有方);edge.From 表示“我的主人是谁”。同样的,Edge 有修饰标签如 .Unique() 表示独一无二。PS:默认允许多个连接(一只奶龙只能有一个贝利亚,而贝利亚可以有很多个奶龙)。具体选项可以自己搜索。
多对多的实现
正常情况下,不只简单从属关系,世界这么大,一般包罗万象。为了表示多对多,Ent 用 Ref(引用)实现。我们用 To 和 From 表示从属,一个 To 对应一个 From,如果再建一对太繁琐。那怎么办?借用现有边呢?答案就是 Ref。
Go
// ent/schema/group.go
type Group struct {
ent.Schema
}

func (Group) Fields() []ent.Field {
return []ent.Field{
field.String(“name”),
}
}

func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To(“users”, User.Type), // Owning: Group -> Users (M2M)
}
}

// ent/schema/user.go
type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String(“name”),
}
}

func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From(“groups”, Group.Type).
Ref(“users”), // Back-ref: User <- Groups (M2M)
}
}
这是一个简单例子:一个人可以归类到很多组,一个组可以拥有很多人,是鲜明的多对多关系。相比刚刚的代码,只多了一个 Ref,表示引用这条边来实现相互指向 QWQ。
对从属关系操作
既然用边实现对象联系,那怎么顺着边查询另一个对象呢?
答案是:关键字 + Schema 名。这样看可能抽象,但看代码就懂了。就拿 Group 和 User 举例:
● 创建/添加:AddUsers()、AddGroups()
● 查询:QueryUsers()、QueryGroups()
● 更新:Update().AddGroups()、RemoveGroups()、ClearGroups()
都是“关键字 + Schema 名”的形式。直接看代码:
Go
import “context” // 假设 ctx = context.Background()

// 创建并链接
hub, _ := client.Group.Create().SetName(“GitHub”).Save(ctx)
a8m, _ := client.User.Create().SetName(“a8m”).AddGroups(hub).Save(ctx) // 添加关系

// 查询用户的组
groups, _ := a8m.QueryGroups().All(ctx)

// 遍历关系(链式查询)
users, _ := a8m.QueryGroups().
Where(group.Not(group.HasUsersWith(user.Name(“nati”)))). // 过滤
QueryUsers().
All(ctx)

// 双向朋友关系(自动互加)
nati, _ := client.User.Create().SetName(“nati”).AddFriends(a8m).Save(ctx)
其中 HasUsersWith 也是关键字 + Schema 的形式,表示“是否有特定用户”(结合刚刚解释,虽然没定义函数但能用)。
详细用法:
● 查询:
○ 直接:a8m.QueryGroups().All(ctx)
○ 过滤:Where(user.HasGroups())(是否有组)、HasUsersWith(user.Name(“nati”))(条件过滤)
○ 计数:a8m.QueryGroups().CountX(ctx)
○ 预加载(避免 N+1):client.User.Query().WithGroups().All(ctx)
○ 遍历:链式如上,支持跨关系查询。
● 更新:
○ 添加:user.Update().AddGroups(group1, group2).Save(ctx)
○ 移除:user.Update().RemoveGroups(group1).Save(ctx)
○ 清空:user.Update().ClearGroups().Save(ctx)(detach 所有关系)
○ 不可变:.Immutable() 选项,使关系只能在创建时设置。
Edge 的高级用法
我们清楚,默认多对多表表示不了所有关系。比如人与人的友谊,不能用简单 Edge 衡量,我们必须体现它的“重量”。于是,在两人联系的 Edge 上添加一个对象,表示友谊属性。请看代码:
Go
// ent/schema/friendship.go
type Friendship struct {
ent.Schema // 嵌入 Ent 的基类,表示这是一个 Schema(类似于数据库表的模板)
}

func (Friendship) Fields() []ent.Field {
return []ent.Field{
field.Time(“since”).Default(time.Now), // 定义一个时间字段 “since”,默认值为当前时间
}
}

// 在 user.go 的 Edges 中
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To(“friends”, User.Type). // 定义 M2M 边的名字和目标类型(这里是自引用,用户到用户)
Through(“friendships”, Friendship.Type), // 指定通过中间 Schema “friendships”(注意小写,Ent 会自动处理表名)
}
}
可以看到,我们定义了一个 Friendship Schema,让人与人之间的联系多了一个“友谊”属性。这个友谊记录了开始时间(当然不止这个,我们也可以加“分量”,就是不好衡量)。
如何保证原子性?事务!
情景: 小学生是个银行柜员,专处理转账。这时奶农来找小学生,要转 50 给 BangBang 请他吃肯德基。小学生想:简单,先扣奶农 50,再加 BangBang 50。但事故发生了!奶农生气:“为什么扣款了,但 BangBang 没收到?!”小学生查日志,发现语句只成功一半:扣款了,但没加钱。
怎么解决?MySQL 用“事务”保证原子性:把操作合成一个单元,中间出错就回滚,不会一边扣款一边没加。
Ent 如何实现事务呢?先看官方注解: Ent 的事务设计遵循“Schema as Code”的原则,通过代码生成提供类型安全的构建器(Builder Pattern),并支持嵌套事务、自定义钩子和错误处理。
基础概念:
● 定义:事务是一个逻辑操作单元,Ent 用 Tx(Transaction Client)封装它。允许临时修改数据库,结束时提交或回滚。
● 为什么需要:并发环境中(如多用户扣库存),单条 SQL 可能数据竞争。事务结合锁确保隔离。
● ACID 支持:
○ 原子性:所有操作作为一个整体。
○ 一致性:操作前后符合约束(如外键)。
○ 隔离性:默认数据库级别(如 READ COMMITTED),可自定义。
○ 持久性:提交后持久化。
● 与 GORM 对比:GORM 用 db.Begin() 和 tx.Commit();Ent 用 client.Tx(ctx),更类型安全(生成方法在 Tx 上可用),避免反射。
总结:先创建事务客户端:

1
2
3
tx, err := client.Tx(ctx)
出错就 tx.Rollback()。其他增删改查一样,但客户端换成 tx。
评鉴代码:
package main

import (
    "context"
    "log"
    "yourproject/ent"
    "yourproject/ent/user"
)

func main() {
    ctx := context.Background()
    client, err := ent.Open("mysql", "your_dsn")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // 开始事务
    tx, err := client.Tx(ctx)
    if err != nil {
        log.Fatal(err)
    }
    // 确保回滚(defer 在函数结束执行)
    defer func() {
        if v := recover(); v != nil {
            tx.Rollback()
            panic(v)  // 重新抛出 panic
        }
        if err := tx.Commit(); err != nil {
            log.Printf("提交失败: %v", err)
        }
    }()

    // 操作1: 从 Nainong 扣 50
    nainong, _ := tx.User.Query().Where(user.Name("Nainong")).Only(ctx)
    if nainong.Balance < 50 {
        panic("余额不足")  // 会触发回滚
    }
    nainong.Update().SetBalance(nainong.Balance - 50).Save(ctx)

    // 操作2: 给 BangBang 加 50
    bangbang, _ := tx.User.Query().Where(user.Name("BangBang")).Only(ctx)
    bangbang.Update().SetBalance(bangbang.Balance + 50).Save(ctx)

    // 无错误,defer 会 Commit
}
相信这样,你会对事务机制理解更深刻。
神奇的上下文
可能大家和我一样疑惑:既然数据不存在 ctx 里,那它有什么用?
官方文档:Go 因为并发设计,鼓励开发者显式传递 ctx。换言之,这是开发者的责任。
通过 context.Context,当不需要操作时,可以直接取消它,或设置超时,避免卡住的不必要操作。
总结:Ent 实现了优雅的 ORM,面向对象操作,借助 Field 和 Edge 表现数据库联系,优雅直观(妈妈再也不用担心我用 GORM 设计多对多表时懵逼了嘻嘻)。优势:避免 GORM 的反射(反射是运行时动态检查 struct,Ent 用生成避免,开销小)。

懒人必备!entorm教程来了!
http://example.com/2026/02/22/懒人必备!entorm教程来了!/
Author
BangBang
Posted on
February 22, 2026
Licensed under