id: schema-edges
title: Edges
简介
边,是实体间的关系(或者是关联)。例如,用户的宠物,群组的用户(成员)。

在上面的例子中,你可以看到两个使用边声明的关系。让我们来实现他们。
1. pets / owner 边; 用户的宠物和宠物的主人:
ent/schema/user.go
package schemaimport ("github.com/facebookincubator/ent""github.com/facebookincubator/ent/schema/edge")// User schema.type User struct {ent.Schema}// Fields of the user.func (User) Fields() []ent.Field {return []ent.Field{// ...}}// Edges of the user.func (User) Edges() []ent.Edge {return []ent.Edge{edge.To("pets", Pet.Type),}}
ent/schema/pet.go
package schemaimport ("github.com/facebookincubator/ent""github.com/facebookincubator/ent/schema/edge")// User schema.type Pet struct {ent.Schema}// Fields of the user.func (Pet) Fields() []ent.Field {return []ent.Field{// ...}}// Edges of the user.func (Pet) Edges() []ent.Edge {return []ent.Edge{edge.From("owner", User.Type).Ref("pets").Unique(),}}
如你所见,一个 User 实体可以拥有 多个 Pet,但是一个 Pet 只能被 一个 User 拥有。
在关系定义时,pets 这条边是一个 02M(一对多)关系,owner 这条边是一个 M20(多对一)关系。
User 模式拥有 pets/owner 关系,因为它使用了 edge.To,而 Pet 模式只是通过 edge.From 和 Ref 方法反向引用了 User.
因为从一个模式到另一个模式可以有多个引用,所以用 Ref 方法指明 Pet 想要引用 User 中的哪一条边,
可以使用 Unique 方法控制边(关系)的类型,下面会有更多的说明。
2. users / groups 边; 群组包含的用户和用户所属的群组。
ent/schema/group.go
package schemaimport ("github.com/facebookincubator/ent""github.com/facebookincubator/ent/schema/edge")// Group schema.type Group struct {ent.Schema}// Fields of the group.func (Group) Fields() []ent.Field {return []ent.Field{// ...}}// Edges of the group.func (Group) Edges() []ent.Edge {return []ent.Edge{edge.To("users", User.Type),}}
ent/schema/user.go
package schemaimport ("github.com/facebookincubator/ent""github.com/facebookincubator/ent/schema/edge")// User schema.type User struct {ent.Schema}// Fields of the user.func (User) Fields() []ent.Field {return []ent.Field{// ...}}// Edges of the user.func (User) Edges() []ent.Edge {return []ent.Edge{edge.From("groups", Group.Type).Ref("users"),// "pets" declared in the example above.edge.To("pets", Pet.Type),}}
如你所见,一个群组实体可以有 多个 用户,并且一个用户也可以属于 多个 群组。
在关系定义时,users 这条边是一个 M2M(多对多)关系,groups 这条件也是一个 M2M 的关系。
To 和 From
edge.To 和 edge.From 是两个用于创建边(关系)的构建器.
在一个关系中,使用 edge.To 定义边的模式,拥有该关系;而使用 edge.From 定义边的模式;只是反向引用了该关系。
继续看一些例子,这些例子示范了如何使用边定义不同的关系。
关系
两种类型的一对一关系

在这个例子中,一个用户只有 一张 信用卡,一张信用卡只能有 一个 户主。
在 User 模式中使用 edge.To 为信用卡定义一条名为 card 的边。并在 Card 模式中使用 edge.From 为用户定义一条名为 owner 的逆边。
ent/schema/user.go
// Edges of the user.func (User) Edges() []ent.Edge {return []ent.Edge{edge.To("card", Card.Type).Unique(),}}
ent/schema/card.go
// Edges of the user.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("creating user: %v", err)}log.Println("user:", a8m)card1, err := client.Card.Create().SetOwner(a8m).SetNumber("1020").SetExpired(time.Now().Add(time.Minute)).Save(ctx)if err != nil {return fmt.Errorf("creating card: %v", err)}log.Println("card:", card1)// 返回满足条件的用户的信用卡,并且期望的数量是 1.card2, err := a8m.QueryCard().Only(ctx)if err != nil {return fmt.Errorf("querying card: %v", err)}log.Println("card:", card2)// 在信用卡实体中,可以通过反向引用查询其户主。owner, err := card2.QueryOwner().Only(ctx)if err != nil {return fmt.Errorf("querying owner: %v", err)}log.Println("owner:", owner)return nil}
完整的例子请查看 GitHub.
同类型的一对一关系

在这个链表例子中,我们有一个名为 next/prev 的递归关系。链表中的每个节点都只有 一个 next和 prev 节点。
如果,节点 A 可以通过 next 指向 节点 B,则节点 B 也可以通过 prev(反向引用) 指向节点 A。
ent/schema/node.go
// Edges of the 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("creating the head: %v", 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` 会 panics.for curr = head; curr != nil; curr = curr.QueryNext().FirstX(ctx) {fmt.Printf("%d ", curr.Value)}// Output: 1 2 3 4 5// 构建循环链表:// 链表的最后一个元素,没有 "next".tail, err := client.Node.Query().Where(node.Not(node.HasNext())).Only(ctx)if err != nil {return fmt.Errorf("getting the tail of the list: %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("getting head's prev: %v", err)}fmt.Printf("\n%v", prev.Value == tail.Value)// Output: truereturn nil}
完整的例子请查看 GitHub.
双向的一对一关系

在这个用户-配偶例子中,我们有一个名为 spouse 的 对称一对一 关系。每个用户只能有 一个配偶。
如果用户 A 的配偶是(使用 spouse 关系) 是 B,那么也可以得知 B 的配偶(使用 spouse 关系)。
注意,在双向关系中,不存在拥有/属于这种说法。
ent/schema/user.go
// Edges of the 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("creating user: %v", err)}nati, err := client.User.Create().SetAge(28).SetName("nati").SetSpouse(a8m).Save(ctx)if err != nil {return fmt.Errorf("creating user: %v", err)}// 查询名为 配偶 的边// 不同于 `Only`, `OnlyX`遇到错误会引起 panics.spouse := nati.QuerySpouse().OnlyX(ctx)fmt.Println(spouse.Name)// Output: a8mspouse = a8m.QuerySpouse().OnlyX(ctx)fmt.Println(spouse.Name)// Output: nati// 查询有配偶用户的数量。// 不同于 `Count`, `CountX`遇到错误会引起 panics.count := client.User.Query().Where(user.HasSpouse()).CountX(ctx)fmt.Println(count)// Output: 2// 获取有配偶,且其配偶姓名为 "a8m" 的用户。spouse = client.User.Query().Where(user.HasSpouseWith(user.Name("a8m"))).OnlyX(ctx)fmt.Println(spouse.Name)// Output: natireturn nil}
完整的例子请查看 GitHub.
不同类型的一对多关系

在这个 用户-宠物 的例子中,用户和宠物之间存在一个 O2M (一对多)关系。
每个用户可以有 多个 宠物,但是一个宠物只有 一个 主人(用户)。如果用户 A 通过 pets 边添加了一个宠物 B,那么,宠物 B 可以通过 owner 边(反向引用边)找到他的主人。
注意,从 Pet (宠物)的角度来说,这就是多对一的关系(M20,many-to-one)。
ent/schema/user.go
// Edges of the User.func (User) Edges() []ent.Edge {return []ent.Edge{edge.To("pets", Pet.Type),}}
ent/schema/pet.go
// Edges of the 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 {// 创建两个宠物。pedro, err := client.Pet.Create().SetName("pedro").Save(ctx)if err != nil {return fmt.Errorf("creating pet: %v", err)}lola, err := client.Pet.Create().SetName("lola").Save(ctx)if err != nil {return fmt.Errorf("creating pet: %v", err)}// 创建用户的同时给他添加两个宠物。a8m, err := client.User.Create().SetAge(30).SetName("a8m").AddPets(pedro, lola).Save(ctx)if err != nil {return fmt.Errorf("creating user: %v", err)}fmt.Println("User created:", a8m)// Output: User(id=1, age=30, name=a8m)// 查询主人,不同于 `Only`, `OnlyX` 遇到错误时会引起 panics.owner := pedro.QueryOwner().OnlyX(ctx)fmt.Println(owner.Name)// Output: a8m// 遍历子图,不同于 `Count`, `CountX` 遇到错误时会引起 panics.count := pedro.QueryOwner(). // a8mQueryPets(). // pedro, lolaCountX(ctx) // countfmt.Println(count)// Output: 2return nil}
完整的例子请查看 GitHub.
同类型的一对多关系

In this example, we have a recursive O2M relation between tree’s nodes and their children (or their parent).
Each node in the tree has many children, and has one parent. If node A adds B to its children,
B can get its owner using the owner edge.
这个例子中,在树的节点及其子节点(或父节点)之间存在一对多(O2M)关系的关系。
树中的每个节点有 多个 子节点,但是它只有 一个 父节点。
如果节点 A 有一个子节点 B,那么节点 B 可以通过 owner 边找到节点 A。
ent/schema/node.go
// Edges of the 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("creating the root: %v", err)}// 构建一颗这样的树://// 2// / \// 1 4// / \// 3 5//// 不同于 `Create`, `CreateX` 遇到错误时会引起 panics.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("Tree leafs", []int{n1.Value, n3.Value, n5.Value})// Output: Tree leafs [1 3 5]// 获取所有叶子节点(没有子节点的节点)。// Unlike `Int`, `IntX` panics if an error occurs.// 不同于 `Int`, `IntX` 遇到错误时会引起 panics.ints := client.Node.Query(). // 全部节点.Where(node.Not(node.HasChildren())). // 叶子节点.Order(ent.Asc(node.FieldValue)). // 根据 `value` 字段升序排序.GroupBy(node.FieldValue). // 仅提取 `value` 字段。IntsX(ctx)fmt.Println(ints)// Output: [1 3 5]// 获取孤儿节点(没有父节点的节点)。// Unlike `Only`, `OnlyX` panics if an error occurs.// 不用于 `Only`, `OnlyX` 遇到错误时会引起 panics.orphan := client.Node.Query().Where(node.Not(node.HasParent())).OnlyX(ctx)fmt.Println(orphan)// Output: Node(id=1, value=2)return nil}
完整的例子请查看 GitHub.
不同类型的多对多关系

在这个例子中,在群组和用户之间存在一个多对多(M2M)的关系。 每个群主可以有 多个 用户,每个用户也可以加入 多个 群组。
ent/schema/group.go
// Edges of the Group.func (Group) Edges() []ent.Edge {return []ent.Edge{edge.To("users", User.Type),}}
ent/schema/user.go
// Edges of the 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` 遇到错误时会引起 panics.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("querying a8m groups: %v", err)}fmt.Println(groups)// Output: [Group(id=1, name=GitHub) Group(id=2, name=GitLab)]groups, err = nati.QueryGroups().All(ctx)if err != nil {return fmt.Errorf("querying nati groups: %v", err)}fmt.Println(groups)// Output: [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("traversing the graph: %v", err)}fmt.Println(users)// Output: [User(id=1, age=30, name=a8m) User(id=2, age=28, name=nati)]return nil}
完整的例子请查看 GitHub.
同类型的多对多关系

下面这个 关注-粉丝 的例子,在用户及其粉丝之间存在一个多对多(M2M)的关系。 每个用户可以关注 多个 用户,也可以有 多个 粉丝。
ent/schema/user.go
// Edges of the 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` 遇到错误时会引起 panics.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)// Output: [User(id=2, age=28, name=nati)]flr := a8m.QueryFollowers().AllX(ctx)fmt.Println(flr)// Output: []flw = nati.QueryFollowing().AllX(ctx)fmt.Println(flw)// Output: []flr = nati.QueryFollowers().AllX(ctx)fmt.Println(flr)// Output: [User(id=1, age=30, name=a8m)]// 图遍历:ages := nati.QueryFollowers(). // [a8m]QueryFollowing(). // [nati]GroupBy(user.FieldAge). // [28]IntsX(ctx)fmt.Println(ages)// Output: [28]names := client.User.Query().Where(user.Not(user.HasFollowers())).GroupBy(user.FieldName).StringsX(ctx)fmt.Println(names)// Output: [a8m]return nil}
完整的例子请查看 GitHub.
双向的多对多关系

In this user-friends example, we have a symmetric M2M relation named friends.
Each user can have many friends. If user A becomes a friend of B, B is also a friend of A.
在这个 用户-朋友 的例子中,存在一个名为 freiends 的双向多对多关系。
每个用户可以有 多个 朋友。如果用户 A 是用户 B 的朋友,那么用户 B 也肯定是用户 A 的朋友。
注意,在双向关系中,不存在拥有/属于这种说法。
ent/schema/user.go
// Edges of the 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` 遇到错误时会引起 panics.a8m := client.User.Create().SetAge(30).SetName("a8m").SaveX(ctx)nati := client.User.Create().SetAge(28).SetName("nati").AddFriends(a8m).SaveX(ctx)// 查询朋友列表。不同于 `All`, `AllX` 遇到错误是会引起 panics.friends := nati.QueryFriends().AllX(ctx)fmt.Println(friends)// Output: [User(id=1, age=30, name=a8m)]friends = a8m.QueryFriends().AllX(ctx)fmt.Println(friends)// Output: [User(id=2, age=28, name=nati)]// 图遍历:friends = client.User.Query().Where(user.HasFriends()).AllX(ctx)fmt.Println(friends)// Output: [User(id=1, age=30, name=a8m) User(id=2, age=28, name=nati)]return nil}
完整的例子请查看 GitHub.
必选项
Edges can be defined as required in the entity creation using the Required method on the builder.
可以使用构建器中的 Required 方法定义关系,使得实体创建时必须满足该关系。
// Edges of the user.func (Card) Edges() []ent.Edge {return []ent.Edge{edge.From("owner", User.Type).Ref("card").Unique().Required(),}}
比如说,无法创建一张没有户主的信用卡。
索引
可以在多个字段或者某些边上添加索引。但是,需要注意的是,目前只有 SQL 支持索引特性。
更多关于索引的内容,可以查阅 索引 部分。
