边(Edges)
快速概览
边(Edges)是实体之间的关系(或关联)。例如,用户的宠物,或群组的用户:
- 图形
- ERD 和 SQL
在上面的示例中,你可以看到使用边声明的 2 个关系。让我们逐一了解。
1. pets / owner 边;用户的宠物和宠物的所有者:
- 用户
- 宠物
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// User 模式。
type User struct {
ent.Schema
}
// 用户的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
// ...
}
}
// 用户的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type),
}
}
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// Pet 持有 Pet 实体的模式定义。
type Pet struct {
ent.Schema
}
// Pet 的字段。
func (Pet) Fields() []ent.Field {
return []ent.Field{
// ...
}
}
// Pet 的边。
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique(),
}
}
如你所见,一个 User 实体可以拥有多个宠物,但一个 Pet 实体只能有一个所有者。
在关系定义中,pets 边是一个 O2M(一对多)关系,而 owner 边是一个 M2O(多对一)关系。
User 模式拥有 pets/owner 关系,因为它使用了 edge.To,而 Pet 模式只是对其有一个反向引用,使用 edge.From 和 Ref 方法声明。
Ref 方法描述了我们在引用 User 模式的哪个边,因为从一个模式到另一个模式可以有多个引用。
边/关系的基数可以使用 Unique 方法控制,下面会进行更广泛的解释。
2. users / groups 边;群组的用户和用户的群组:
- 群组
- 用户
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// Group 模式。
type Group struct {
ent.Schema
}
// 群组的字段。
func (Group) Fields() []ent.Field {
return []ent.Field{
// ...
}
}
// 群组的边。
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
)
// User 模式。
type User struct {
ent.Schema
}
// 用户的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
// ...
}
}
// 用户的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("groups", Group.Type).
Ref("users"),
// 上面示例中声明的 "pets"。
edge.To("pets", Pet.Type),
}
}
如你所见,一个 Group 实体可以拥有多个用户,而一个 User 实体可以拥有多个群组。
在关系定义中,users 边是一个 M2M(多对多)关系,而 groups 边也是一个 M2M(多对多)关系。
To 和 From
edge.To 和 edge.From 是创建边/关系的两个构建器。
使用 edge.To 构建器定义边的模式拥有该关系,这与使用 edge.From 构建器不同,后者仅提供关系的反向引用(使用不同的名称)。
让我们看几个示例,展示如何使用边定义不同的关系类型。
关系类型
O2O 两种类型
- 图形
- ERD 和 SQL
在此示例中,一个用户只有一张信用卡,而一张卡只有一个所有者。
User 模式定义了一个名为 card 的 edge.To 边,而 Card 模式使用 edge.From 定义了一个名为 owner 的反向引用到此边。
- 用户
- 卡
// 用户的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("card", Card.Type).
Unique(),
}
}
// Card 的边。
func (Card) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("card").
Unique().
// 我们在构建器上添加 "Required" 方法,
// 使此边在实体创建时为必需。
// 即,没有所有者就无法创建卡。
Required(),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
a8m, err := client.User.
Create().
SetAge(30).
SetName("Mashraki").
Save(ctx)
if err != nil {
return fmt.Errorf("创建用户: %w", err)
}
log.Println("用户:", a8m)
card1, err := client.Card.
Create().
SetOwner(a8m).
SetNumber("1020").
SetExpired(time.Now().Add(time.Minute)).
Save(ctx)
if err != nil {
return fmt.Errorf("创建卡: %w", err)
}
log.Println("卡:", card1)
// 仅返回用户的卡,
// 并期望只有一张。
card2, err := a8m.QueryCard().Only(ctx)
if err != nil {
return fmt.Errorf("查询卡: %w", err)
}
log.Println("卡:", card2)
// Card 实体能够使用其反向引用查询其所有者。
owner, err := card2.QueryOwner().Only(ctx)
if err != nil {
return fmt.Errorf("查询所有者: %w", err)
}
log.Println("所有者:", owner)
return nil
}
完整示例存在于 GitHub。
O2O 相同类型
- 图形
- ERD 和 SQL
在这个链表示例中,我们有一个名为 next/prev 的递归关系。列表中的每个节点只能有一个 next 节点。如果节点 A 指向(使用 next)节点 B,B 可以使用 prev(反向引用边)获取其指针。
// Node 的边。
func (Node) Edges() []ent.Edge {
return []ent.Edge{
edge.To("next", Node.Type).
Unique().
From("prev").
Unique(),
}
}
如你所见,在相同类型的关系中,你可以在同一个构建器中声明边及其引用。
func (Node) Edges() []ent.Edge {
return []ent.Edge{
+ edge.To("next", Node.Type).
+ Unique().
+ From("prev").
+ Unique(),
- edge.To("next", Node.Type).
- Unique(),
- edge.From("prev", Node.Type).
- Ref("next").
- Unique(),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
head, err := client.Node.
Create().
SetValue(1).
Save(ctx)
if err != nil {
return fmt.Errorf("创建头节点: %w", err)
}
curr := head
// 生成以下链表:1<->2<->3<->4<->5。
for i := 0; i < 4; i++ {
curr, err = client.Node.
Create().
SetValue(curr.Value + 1).
SetPrev(curr).
Save(ctx)
if err != nil {
return err
}
}
// 遍历列表并打印。`FirstX` 在出错时 panic。
for curr = head; curr != nil; curr = curr.QueryNext().FirstX(ctx) {
fmt.Printf("%d ", curr.Value)
}
// 输出: 1 2 3 4 5
// 使链表循环:
// 列表的尾部没有 "next"。
tail, err := client.Node.
Query().
Where(node.Not(node.HasNext())).
Only(ctx)
if err != nil {
return fmt.Errorf("获取列表尾部: %v", tail)
}
tail, err = tail.Update().SetNext(head).Save(ctx)
if err != nil {
return err
}
// 检查更改是否实际应用:
prev, err := head.QueryPrev().Only(ctx)
if err != nil {
return fmt.Errorf("获取头节点的 prev: %w", err)
}
fmt.Printf("\n%v", prev.Value == tail.Value)
// 输出: true
return nil
}
完整示例存在于 GitHub。
O2O 双向
- 图形
- ERD 和 SQL
在这个用户-配偶示例中,我们有一个名为 spouse 的对称 O2O 关系。每个用户只能有一个配偶。
如果用户 A 将其配偶(使用 spouse)设置为 B,B 可以使用 spouse 边获取其配偶。
注意,在双向边的情况下,没有所有者/反向术语。
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("spouse", User.Type).
Unique(),
}
}
与此边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
a8m, err := client.User.
Create().
SetAge(30).
SetName("a8m").
Save(ctx)
if err != nil {
return fmt.Errorf("创建用户: %w", err)
}
nati, err := client.User.
Create().
SetAge(28).
SetName("nati").
SetSpouse(a8m).
Save(ctx)
if err != nil {
return fmt.Errorf("创建用户: %w", err)
}
// 查询配偶边。
// 与 `Only` 不同,`OnlyX` 在出错时 panic。
spouse := nati.QuerySpouse().OnlyX(ctx)
fmt.Println(spouse.Name)
// 输出: a8m
spouse = a8m.QuerySpouse().OnlyX(ctx)
fmt.Println(spouse.Name)
// 输出: nati
// 查询有多少用户有配偶。
// 与 `Count` 不同,`CountX` 在出错时 panic。
count := client.User.
Query().
Where(user.HasSpouse()).
CountX(ctx)
fmt.Println(count)
// 输出: 2
// 获取有配偶且配偶名称为 "a8m" 的用户。
spouse = client.User.
Query().
Where(user.HasSpouseWith(user.Name("a8m"))).
OnlyX(ctx)
fmt.Println(spouse.Name)
// 输出: nati
return nil
}
注意,外键列可以通过边字段选项进行配置并作为实体字段公开,如下所示:
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("spouse_id").
Optional(),
}
}
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("spouse", User.Type).
Unique().
Field("spouse_id"),
}
}
完整示例存在于 GitHub。
O2M 两种类型
- 图形
- ERD 和 SQL
在这个用户-宠物示例中,我们有一个用户与其宠物之间的 O2M 关系。
每个用户拥有多个宠物,而一个宠物有一个所有者。
如果用户 A 使用 pets 边添加宠物 B,B 可以使用 owner 边(反向引用边)获取其所有者。
注意,从 Pet 模式的角度来看,这种关系也是 M2O(多对一)。
- 用户
- 宠物
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type),
}
}
// Pet 的边。
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique(),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
// 创建 2 个宠物。
pedro, err := client.Pet.
Create().
SetName("pedro").
Save(ctx)
if err != nil {
return fmt.Errorf("创建宠物: %w", err)
}
lola, err := client.Pet.
Create().
SetName("lola").
Save(ctx)
if err != nil {
return fmt.Errorf("创建宠物: %w", err)
}
// 创建用户,并在创建时添加其宠物。
a8m, err := client.User.
Create().
SetAge(30).
SetName("a8m").
AddPets(pedro, lola).
Save(ctx)
if err != nil {
return fmt.Errorf("创建用户: %w", err)
}
fmt.Println("用户已创建:", a8m)
// 输出: User(id=1, age=30, name=a8m)
// 查询所有者。与 `Only` 不同,`OnlyX` 在出错时 panic。
owner := pedro.QueryOwner().OnlyX(ctx)
fmt.Println(owner.Name)
// 输出: a8m
// 遍历子图。与 `Count` 不同,`CountX` 在出错时 panic。
count := pedro.
QueryOwner(). // a8m
QueryPets(). // pedro, lola
CountX(ctx) // 计数
fmt.Println(count)
// 输出: 2
return nil
}
注意,外键列可以通过边字段选项进行配置并作为实体字段公开,如下所示:
// Pet 的字段。
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.Int("owner_id").
Optional(),
}
}
// Pet 的边。
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique().
Field("owner_id"),
}
}
完整示例存在于 GitHub。
O2M 相同类型
- 图形
- ERD 和 SQL
在此示例中,我们有一个树节点与其子节点(或其父节点)之间的递归 O2M 关系。
树中的每个节点拥有多个子节点,并有一个父节点。如果节点 A 将 B 添加到其子节点,
B 可以使用 owner 边获取其所有者。
// Node 的边。
func (Node) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Node.Type).
From("parent").
Unique(),
}
}
如你所见,在相同类型的关系中,你可以在同一个构建器中声明边及其引用。
func (Node) Edges() []ent.Edge {
return []ent.Edge{
+ edge.To("children", Node.Type).
+ From("parent").
+ Unique(),
- edge.To("children", Node.Type),
- edge.From("parent", Node.Type).
- Ref("children").
- Unique(),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
root, err := client.Node.
Create().
SetValue(2).
Save(ctx)
if err != nil {
return fmt.Errorf("创建根节点: %w", err)
}
// 向树添加额外节点:
//
// 2
// / \
// 1 4
// / \
// 3 5
//
// 与 `Save` 不同,`SaveX` 在出错时 panic。
n1 := client.Node.
Create().
SetValue(1).
SetParent(root).
SaveX(ctx)
n4 := client.Node.
Create().
SetValue(4).
SetParent(root).
SaveX(ctx)
n3 := client.Node.
Create().
SetValue(3).
SetParent(n4).
SaveX(ctx)
n5 := client.Node.
Create().
SetValue(5).
SetParent(n4).
SaveX(ctx)
fmt.Println("树叶节点", []int{n1.Value, n3.Value, n5.Value})
// 输出: 树叶节点 [1 3 5]
// 获取所有叶节点(没有子节点的节点)。
// 与 `Int` 不同,`IntX` 在出错时 panic。
ints := client.Node.
Query(). // 所有节点。
Where(node.Not(node.HasChildren())). // 仅叶节点。
Order(ent.Asc(node.FieldValue)). // 按其 `value` 字段排序。
GroupBy(node.FieldValue). // 仅提取 `value` 字段。
IntsX(ctx)
fmt.Println(ints)
// 输出: [1 3 5]
// 获取孤儿节点(没有父节点的节点)。
// 与 `Only` 不同,`OnlyX` 在出错时 panic。
orphan := client.Node.
Query().
Where(node.Not(node.HasParent())).
OnlyX(ctx)
fmt.Println(orphan)
// 输出: Node(id=1, value=2)
return nil
}
注意,外键列可以通过边字段选项进行配置并作为实体字段公开,如下所示:
// Node 的字段。
func (Node) Fields() []ent.Field {
return []ent.Field{
field.Int("parent_id").
Optional(),
}
}
// Node 的边。
func (Node) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Node.Type).
From("parent").
Unique().
Field("parent_id"),
}
}
完整示例存在于 GitHub。
M2M 两种类型
- 图形
- ERD 和 SQL
在这个群组-用户示例中,我们有一个群组与其用户之间的 M2M 关系。 每个群组拥有多个用户,而每个用户可以加入多个群组。
// Group 的边。
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("groups", Group.Type).
Ref("users"),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
// 与 `Save` 不同,`SaveX` 在出错时 panic。
hub := client.Group.
Create().
SetName("GitHub").
SaveX(ctx)
lab := client.Group.
Create().
SetName("GitLab").
SaveX(ctx)
a8m := client.User.
Create().
SetAge(30).
SetName("a8m").
AddGroups(hub, lab).
SaveX(ctx)
nati := client.User.
Create().
SetAge(28).
SetName("nati").
AddGroups(hub).
SaveX(ctx)
// 查询边。
groups, err := a8m.
QueryGroups().
All(ctx)
if err != nil {
return fmt.Errorf("查询 a8m 的群组: %w", err)
}
fmt.Println(groups)
// 输出: [Group(id=1, name=GitHub) Group(id=2, name=GitLab)]
groups, err = nati.
QueryGroups().
All(ctx)
if err != nil {
return fmt.Errorf("查询 nati 的群组: %w", err)
}
fmt.Println(groups)
// 输出: [Group(id=1, name=GitHub)]
// 遍历图。
users, err := a8m.
QueryGroups(). // [hub, lab]
Where(group.Not(group.HasUsersWith(user.Name("nati")))). // [lab]
QueryUsers(). // [a8m]
QueryGroups(). // [hub, lab]
QueryUsers(). // [a8m, nati]
All(ctx)
if err != nil {
return fmt.Errorf("遍历图: %w", err)
}
fmt.Println(users)
// 输出: [User(id=1, age=30, name=a8m) User(id=2, age=28, name=nati)]
return nil
}
调用 AddGroups(一个 M2M 边)在边已存在且不是边模式时会导致无操作:
a8m := client.User.
Create().
SetName("a8m").
AddGroups(
hub,
hub, // 无操作。
).
SaveX(ctx)
完整示例存在于 GitHub。
M2M 相同类型
- 图形
- ERD 和 SQL
在这个关注者-被关注者示例中,我们有一个用户与其关注者之间的 M2M 关系。每个用户 可以关注多个用户,并可以有多个关注者。
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("following", User.Type).
From("followers"),
}
}
如你所见,在相同类型的关系中,你可以在同一个构建器中声明边及其引用。
func (User) Edges() []ent.Edge {
return []ent.Edge{
+ edge.To("following", User.Type).
+ From("followers"),
- edge.To("following", User.Type),
- edge.From("followers", User.Type).
- Ref("following"),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
// 与 `Save` 不同,`SaveX` 在出错时 panic。
a8m := client.User.
Create().
SetAge(30).
SetName("a8m").
SaveX(ctx)
nati := client.User.
Create().
SetAge(28).
SetName("nati").
AddFollowers(a8m).
SaveX(ctx)
// 查询关注/被关注者:
flw := a8m.QueryFollowing().AllX(ctx)
fmt.Println(flw)
// 输出: [User(id=2, age=28, name=nati)]
flr := a8m.QueryFollowers().AllX(ctx)
fmt.Println(flr)
// 输出: []
flw = nati.QueryFollowing().AllX(ctx)
fmt.Println(flw)
// 输出: []
flr = nati.QueryFollowers().AllX(ctx)
fmt.Println(flr)
// 输出: [User(id=1, age=30, name=a8m)]
// 遍历图:
ages := nati.
QueryFollowers(). // [a8m]
QueryFollowing(). // [nati]
GroupBy(user.FieldAge). // [28]
IntsX(ctx)
fmt.Println(ages)
// 输出: [28]
names := client.User.
Query().
Where(user.Not(user.HasFollowers())).
GroupBy(user.FieldName).
StringsX(ctx)
fmt.Println(names)
// 输出: [a8m]
return nil
}
调用 AddFollowers(一个 M2M 边)在边已存在且不是边模式时会导致无操作:
a8m := client.User.
Create().
SetName("a8m").
AddFollowers(
nati,
nati, // 无操作。
).
SaveX(ctx)
完整示例存在于 GitHub。
M2M 双向
- 图形
- ERD 和 SQL
在这个用户-朋友示例中,我们有一个名为 friends 的对称 M2M 关系。
每个用户可以拥有多个朋友。如果用户 A 成为 B 的朋友,B 也是 A 的朋友。
注意,在双向边的情况下,没有所有者/反向术语。
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("friends", User.Type),
}
}
与这些边交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
// 与 `Save` 不同,`SaveX` 在出错时 panic。
a8m := client.User.
Create().
SetAge(30).
SetName("a8m").
SaveX(ctx)
nati := client.User.
Create().
SetAge(28).
SetName("nati").
AddFriends(a8m).
SaveX(ctx)
// 查询朋友。与 `All` 不同,`AllX` 在出错时 panic。
friends := nati.
QueryFriends().
AllX(ctx)
fmt.Println(friends)
// 输出: [User(id=1, age=30, name=a8m)]
friends = a8m.
QueryFriends().
AllX(ctx)
fmt.Println(friends)
// 输出: [User(id=2, age=28, name=nati)]
// 查询图:
friends = client.User.
Query().
Where(user.HasFriends()).
AllX(ctx)
fmt.Println(friends)
// 输出: [User(id=1, age=30, name=a8m) User(id=2, age=28, name=nati)]
return nil
}
调用 AddFriends(一个 M2M 双向边)在边已存在且不是边模式时会导致无操作:
a8m := client.User.
Create().
SetName("a8m").
AddFriends(
nati,
nati, // 无操作。
).
SaveX(ctx)
完整示例存在于 GitHub。
边字段
边的 Field 选项允许用户在模式上将外键作为常规字段公开。
注意,只有持有外键(边 ID)的关系才允许使用此选项。
// Post 的字段。
func (Post) Fields() []ent.Field {
return []ent.Field{
field.Int("author_id").
Optional(),
}
}
// Post 的边。
func (Post) Edges() []ent.Edge {
return []ent.Edge{
edge.To("author", User.Type).
// 将 "author_id" 字段绑定到此边。
Field("author_id").
Unique(),
}
}
与边字段交互的 API 如下:
func Do(ctx context.Context, client *ent.Client) error {
p, err := c.Post.Query().
Where(post.AuthorID(id)).
OnlyX(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(p.AuthorID) // 访问 "author" 外键。
}
多个示例存在于 GitHub。
迁移到边字段
如 StorageKey 部分所述,Ent 通过 edge.To 配置边存储键(例如外键)。因此,如果你想向现有边(在数据库中已作为列存在)添加字段,你需要使用 StorageKey 选项进行设置,如下所示:
// Post 的字段。
func (Post) Fields() []ent.Field {
return []ent.Field{
+ field.Int("author_id").
+ Optional(),
}
}
// Post 的边。
func (Post) Edges() []ent.Edge {
return []ent.Edge{
edge.From("author", User.Type).
+ Field("author_id").
+ StorageKey(edge.Column("post_author")).
Unique(),
}
}
或者,此选项可以在边字段上配置:
// Post 的字段。
func (Post) Fields() []ent.Field {
return []ent.Field{
+ field.Int("author_id").
+ StorageKey("post_author").
+ Optional(),
}
}
如果你不确定在使用边字段选项之前外键是如何命名的,
请查看你项目中生成的模式描述:<project>/ent/migrate/schema.go。
边模式
边模式是用于 M2M 边的中间实体模式。通过使用 Through 选项,用户可以为关系定义边模式。这允许用户在其公共 API 中公开关系,存储额外字段,应用 CRUD 操作,以及在边上设置钩子和隐私策略。
用户友谊示例
在以下示例中,我们演示如何使用边模式建模两个用户之间的友谊,该模式具有关系的两个必需字段(user_id 和 friend_id),以及一个名为 created_at 的额外字段,其值在创建时自动设置。
- 图形
- ERD 和 SQL
- User
- Friendship
// User 持有 User 实体的模式定义。
type User struct {
ent.Schema
}
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Default("Unknown"),
}
}
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("friends", User.Type).
Through("friendships", Friendship.Type),
}
}
// Friendship 持有 Friendship 关系的边模式定义。
type Friendship struct {
ent.Schema
}
// Friendship 的字段。
func (Friendship) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.Int("user_id"),
field.Int("friend_id"),
}
}
// Friendship 的边。
func (Friendship) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user", User.Type).
Required().
Unique().
Field("user_id"),
edge.To("friend", User.Type).
Required().
Unique().
Field("friend_id"),
}
}
- 与实体模式类似,如果未另行说明,
ID字段会自动为边模式生成。 - 边模式不能被多个关系使用。
- 边模式中的
user_id和friend_id边字段是必需的,因为它们构成了关系。 :::
用户点赞示例
在以下示例中,我们演示如何建模一个系统,其中用户可以“点赞”推文,并且推文被“点赞”的时间戳存储在数据库中。这是在边上存储额外字段的一种方式。
- User
- Tweet
- Like
// User 持有 User 实体的模式定义。
type User struct {
ent.Schema
}
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Default("Unknown"),
}
}
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("liked_tweets", Tweet.Type).
Through("likes", Like.Type),
}
}
// Tweet 持有 Tweet 实体的模式定义。
type Tweet struct {
ent.Schema
}
// Tweet 的字段。
func (Tweet) Fields() []ent.Field {
return []ent.Field{
field.Text("text"),
}
}
// Tweet 的边。
func (Tweet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("liked_users", User.Type).
Ref("liked_tweets").
Through("likes", Like.Type),
}
}
// Like 持有 Like 边的边模式定义。
type Like struct {
ent.Schema
}
func (Like) Annotations() []schema.Annotation {
return []schema.Annotation{
field.ID("user_id", "tweet_id"),
}
}
// Like 的字段。
func (Like) Fields() []ent.Field {
return []ent.Field{
field.Time("liked_at").
Default(time.Now),
field.Int("user_id"),
field.Int("tweet_id"),
}
}
// Like 的边。
func (Like) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user", User.Type).
Unique().
Required().
Field("user_id"),
edge.To("tweet", Tweet.Type).
Unique().
Required().
Field("tweet_id"),
}
}
在上面的示例中,field.ID 注解用于告诉 Ent 边模式标识符是
两个边字段 user_id 和 tweet_id 的复合主键。因此,ID 字段将
不会为 Like 结构及其任何构建器方法生成。例如 Get、OnlyID 等。
:::
边模式在其他边类型中的使用
在某些情况下,用户希望将 O2M/M2O 或 O2O 关系存储在单独的表中(即连接表),以便 简化未来的迁移,以防边类型发生变化。例如,希望通过删除唯一约束将 O2M/M2O 边更改为 M2M,而不是将外键值迁移到新表。
在以下示例中,我们展示了一个模型,其中用户可以“创作”推文,并带有约束:一条推文只能由
一个用户编写。与常规的 O2M/M2O 边不同,通过使用边模式,我们在连接表上使用 tweet_id 列的唯一索引来强制执行此约束。此约束可能在将来被删除,以允许多个
用户参与推文的“创作”。因此,将边类型更改为 M2M 而无需将数据迁移到新表。
- User
- Tweet
- UserTweet
// User 持有 User 实体的模式定义。
type User struct {
ent.Schema
}
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Default("Unknown"),
}
}
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("tweets", Tweet.Type).
Through("user_tweets", UserTweet.Type),
}
}
// Tweet 持有 Tweet 实体的模式定义。
type Tweet struct {
ent.Schema
}
// Tweet 的字段。
func (Tweet) Fields() []ent.Field {
return []ent.Field{
field.Text("text"),
}
}
// Tweet 的边。
func (Tweet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type).
Ref("tweets").
Through("tweet_user", UserTweet.Type).
Comment("作者的唯一性在边模式上强制执行"),
}
}
// UserTweet 持有 UserTweet 实体的模式定义。
type UserTweet struct {
ent.Schema
}
// UserTweet 的字段。
func (UserTweet) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.Int("user_id"),
field.Int("tweet_id"),
}
}
// UserTweet 的边。
func (UserTweet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user", User.Type).
Unique().
Required().
Field("user_id"),
edge.To("tweet", Tweet.Type).
Unique().
Required().
Field("tweet_id"),
}
}
// UserTweet 的索引。
func (UserTweet) Indexes() []ent.Index {
return []ent.Index{
index.Fields("tweet_id").
Unique(),
}
}
必需
边可以使用构建器上的 Required 方法定义为实体创建时所必需。
// Card 的边。
func (Card) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("card").
Unique().
Required(),
}
}
在上面的示例中,没有所有者就无法创建卡实体。
不可变
不可变边是只能在实体创建时设置或添加的边。 即,不会为实体的更新构建器生成 setter 方法。
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("tenant", Tenant.Type).
Field("tenant_id").
Unique().
Required().
Immutable(),
}
}
StorageKey
默认情况下,Ent 通过边所有者(持有 edge.To 的模式)配置边存储键,而不是通过反向引用(edge.From)。这是因为反向引用是可选的,可以被移除。
为了使用边的自定义存储配置,请使用 StorageKey 方法,如下所示:
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
// 设置 O2M 关系中 "pets" 表的列名。
StorageKey(edge.Column("owner_id")),
edge.To("cars", Car.Type).
// 设置 O2M 关系中外键约束的符号。
StorageKey(edge.Symbol("cars_owner_id")),
edge.To("friends", User.Type).
// 设置 M2M 关系的连接表和列名。
StorageKey(edge.Table("friends"), edge.Columns("user_id", "friend_id")),
edge.To("groups", Group.Type).
// 设置 M2M 关系的连接表、其列名和外键约束的符号。
StorageKey(
edge.Table("groups"),
edge.Columns("user_id", "group_id"),
edge.Symbols("groups_id1", "groups_id2")
),
}
}
结构体标签
可以使用 StructTag 方法向生成的实体添加自定义结构体标签。
注意,如果未提供此选项,或提供但未包含 json 标签,则将添加带有字段名称的默认 json 标签。
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
// 将默认的 json 标签 "pets" 覆盖为 "owner",用于 O2M 关系。
StructTag(`json:"owner"`),
}
}
索引
可以在多字段和某些类型的边上定义索引。 但是,你应该注意,这目前是仅 SQL 功能。
在 索引 部分阅读更多关于此的内容。
注释
可以使用 .Comment() 方法向边添加注释。此注释
出现在生成的实体代码中的边之前。使用 \n 转义序列支持换行。
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
Comment("此用户负责照顾的宠物。\n" +
"可能为零到多个,取决于用户。")
}
}
注解
Annotations 用于在代码生成中将任意元数据附加到边对象。
模板扩展可以检索此元数据并在其模板中使用。
注意,元数据对象必须可序列化为 JSON 原始值(例如结构体、映射或切片)。
// Pet 模式。
type Pet struct {
ent.Schema
}
// Pet 的边。
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Ref("pets").
Unique().
Annotations(entgql.RelayConnection()),
}
}
在 模板文档 中阅读更多关于注解及其在模板中的用法。
命名约定
按照约定,边名称应使用 snake_case。由 ent 生成的相应结构体字段将遵循 Go 约定,使用 PascalCase。在需要 PascalCase 的情况下,你可以使用 StorageKey 或 StructTag 方法来实现。



















