跳到主要内容

边(Edges)

快速概览

边(Edges)是实体之间的关系(或关联)。例如,用户的宠物,或群组的用户:

在上面的示例中,你可以看到使用边声明的 2 个关系。让我们逐一了解。

1. pets / owner 边;用户的宠物和宠物的所有者:

ent/schema/user.go
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),
}
}

如你所见,一个 User 实体可以拥有多个宠物,但一个 Pet 实体只能有一个所有者。 在关系定义中,pets 边是一个 O2M(一对多)关系,而 owner 边是一个 M2O(多对一)关系。

User 模式拥有 pets/owner 关系,因为它使用了 edge.To,而 Pet 模式只是对其有一个反向引用,使用 edge.FromRef 方法声明。

Ref 方法描述了我们在引用 User 模式的哪个边,因为从一个模式到另一个模式可以有多个引用。

边/关系的基数可以使用 Unique 方法控制,下面会进行更广泛的解释。

2. users / groups 边;群组的用户和用户的群组:

ent/schema/group.go
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),
}
}

如你所见,一个 Group 实体可以拥有多个用户,而一个 User 实体可以拥有多个群组。 在关系定义中,users 边是一个 M2M(多对多)关系,而 groups 边也是一个 M2M(多对多)关系。

To 和 From

edge.Toedge.From 是创建边/关系的两个构建器。

使用 edge.To 构建器定义边的模式拥有该关系,这与使用 edge.From 构建器不同,后者仅提供关系的反向引用(使用不同的名称)。

让我们看几个示例,展示如何使用边定义不同的关系类型。

关系类型

O2O 两种类型

在此示例中,一个用户只有一张信用卡,而一张卡只有一个所有者。

User 模式定义了一个名为 cardedge.To 边,而 Card 模式使用 edge.From 定义了一个名为 owner 的反向引用到此边。

ent/schema/user.go
// 用户的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("card", Card.Type).
Unique(),
}
}

与这些边交互的 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 相同类型

在这个链表示例中,我们有一个名为 next/prev递归关系。列表中的每个节点只能有一个 next 节点。如果节点 A 指向(使用 next)节点 B,B 可以使用 prev(反向引用边)获取其指针。

ent/schema/node.go
// 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 双向

在这个用户-配偶示例中,我们有一个名为 spouse对称 O2O 关系。每个用户只能有一个配偶。 如果用户 A 将其配偶(使用 spouse)设置为 B,B 可以使用 spouse 边获取其配偶。

注意,在双向边的情况下,没有所有者/反向术语。

ent/schema/user.go
// 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 两种类型

在这个用户-宠物示例中,我们有一个用户与其宠物之间的 O2M 关系。 每个用户拥有多个宠物,而一个宠物有一个所有者。 如果用户 A 使用 pets 边添加宠物 B,B 可以使用 owner 边(反向引用边)获取其所有者。

注意,从 Pet 模式的角度来看,这种关系也是 M2O(多对一)。

ent/schema/user.go
// User 的边。
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type),
}
}

与这些边交互的 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
}

注意,外键列可以通过边字段选项进行配置并作为实体字段公开,如下所示:

ent/schema/pet.go
// 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 相同类型

在此示例中,我们有一个树节点与其子节点(或其父节点)之间的递归 O2M 关系。 树中的每个节点拥有多个子节点,并有一个父节点。如果节点 A 将 B 添加到其子节点, B 可以使用 owner 边获取其所有者。

ent/schema/node.go
// 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 两种类型

在这个群组-用户示例中,我们有一个群组与其用户之间的 M2M 关系。 每个群组拥有多个用户,而每个用户可以加入多个群组。

ent/schema/group.go
// Group 的边。
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
ent/schema/user.go
// 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 相同类型

在这个关注者-被关注者示例中,我们有一个用户与其关注者之间的 M2M 关系。每个用户 可以关注多个用户,并可以有多个关注者。

ent/schema/user.go
// 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 双向

在这个用户-朋友示例中,我们有一个名为 friends对称 M2M 关系。 每个用户可以拥有多个朋友。如果用户 A 成为 B 的朋友,B 也是 A 的朋友。

注意,在双向边的情况下,没有所有者/反向术语。

ent/schema/user.go
// 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)的关系才允许使用此选项。

ent/schema/post.go
// 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_idfriend_id),以及一个名为 created_at 的额外字段,其值在创建时自动设置。

ent/schema/user.go
// 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),
}
}
信息
  • 与实体模式类似,如果未另行说明,ID 字段会自动为边模式生成。
  • 边模式不能被多个关系使用。
  • 边模式中的 user_idfriend_id 边字段是必需的,因为它们构成了关系。 :::

用户点赞示例

在以下示例中,我们演示如何建模一个系统,其中用户可以“点赞”推文,并且推文被“点赞”的时间戳存储在数据库中。这是在边上存储额外字段的一种方式。

ent/schema/user.go
// 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),
}
}

在上面的示例中,field.ID 注解用于告诉 Ent 边模式标识符是 两个边字段 user_idtweet_id 的复合主键。因此,ID 字段将 不会为 Like 结构及其任何构建器方法生成。例如 GetOnlyID 等。 :::

边模式在其他边类型中的使用

在某些情况下,用户希望将 O2M/M2O 或 O2O 关系存储在单独的表中(即连接表),以便 简化未来的迁移,以防边类型发生变化。例如,希望通过删除唯一约束将 O2M/M2O 边更改为 M2M,而不是将外键值迁移到新表。

在以下示例中,我们展示了一个模型,其中用户可以“创作”推文,并带有约束:一条推文只能由 一个用户编写。与常规的 O2M/M2O 边不同,通过使用边模式,我们在连接表上使用 tweet_id 列的唯一索引来强制执行此约束。此约束可能在将来被删除,以允许多个 用户参与推文的“创作”。因此,将边类型更改为 M2M 而无需将数据迁移到新表。

ent/schema/user.go
// 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),
}
}

必需

边可以使用构建器上的 Required 方法定义为实体创建时所必需。

// Card 的边。
func (Card) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("card").
Unique().
Required(),
}
}

在上面的示例中,没有所有者就无法创建卡实体。

信息

注意,从 v0.10 开始,对于不是自引用的必需边,外键列在数据库中创建为 NOT NULL。为了迁移现有的外键列,请使用 Atlas 迁移 选项。

不可变

不可变边是只能在实体创建时设置或添加的边。 即,不会为实体的更新构建器生成 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 的情况下,你可以使用 StorageKeyStructTag 方法来实现。