id: schema-edges

title: Edges

简介

边,是实体间的关系(或者是关联)。例如,用户的宠物,群组的用户(成员)。

er-group-users

在上面的例子中,你可以看到两个使用边声明的关系。让我们来实现他们。

1. pets / owner 边; 用户的宠物和宠物的主人:

ent/schema/user.go

  1. package schema
  2. import (
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/edge"
  5. )
  6. // User schema.
  7. type User struct {
  8. ent.Schema
  9. }
  10. // Fields of the user.
  11. func (User) Fields() []ent.Field {
  12. return []ent.Field{
  13. // ...
  14. }
  15. }
  16. // Edges of the user.
  17. func (User) Edges() []ent.Edge {
  18. return []ent.Edge{
  19. edge.To("pets", Pet.Type),
  20. }
  21. }

ent/schema/pet.go

  1. package schema
  2. import (
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/edge"
  5. )
  6. // User schema.
  7. type Pet struct {
  8. ent.Schema
  9. }
  10. // Fields of the user.
  11. func (Pet) Fields() []ent.Field {
  12. return []ent.Field{
  13. // ...
  14. }
  15. }
  16. // Edges of the user.
  17. func (Pet) Edges() []ent.Edge {
  18. return []ent.Edge{
  19. edge.From("owner", User.Type).
  20. Ref("pets").
  21. Unique(),
  22. }
  23. }

如你所见,一个 User 实体可以拥有 多个 Pet,但是一个 Pet 只能被 一个 User 拥有。 在关系定义时,pets 这条边是一个 02M(一对多)关系,owner 这条边是一个 M20(多对一)关系。

User 模式拥有 pets/owner 关系,因为它使用了 edge.To,而 Pet 模式只是通过 edge.FromRef 方法反向引用了 User.

因为从一个模式到另一个模式可以有多个引用,所以用 Ref 方法指明 Pet 想要引用 User 中的哪一条边,

可以使用 Unique 方法控制边(关系)的类型,下面会有更多的说明。

2. users / groups 边; 群组包含的用户和用户所属的群组。

ent/schema/group.go

  1. package schema
  2. import (
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/edge"
  5. )
  6. // Group schema.
  7. type Group struct {
  8. ent.Schema
  9. }
  10. // Fields of the group.
  11. func (Group) Fields() []ent.Field {
  12. return []ent.Field{
  13. // ...
  14. }
  15. }
  16. // Edges of the group.
  17. func (Group) Edges() []ent.Edge {
  18. return []ent.Edge{
  19. edge.To("users", User.Type),
  20. }
  21. }

ent/schema/user.go

  1. package schema
  2. import (
  3. "github.com/facebookincubator/ent"
  4. "github.com/facebookincubator/ent/schema/edge"
  5. )
  6. // User schema.
  7. type User struct {
  8. ent.Schema
  9. }
  10. // Fields of the user.
  11. func (User) Fields() []ent.Field {
  12. return []ent.Field{
  13. // ...
  14. }
  15. }
  16. // Edges of the user.
  17. func (User) Edges() []ent.Edge {
  18. return []ent.Edge{
  19. edge.From("groups", Group.Type).
  20. Ref("users"),
  21. // "pets" declared in the example above.
  22. edge.To("pets", Pet.Type),
  23. }
  24. }

如你所见,一个群组实体可以有 多个 用户,并且一个用户也可以属于 多个 群组。 在关系定义时,users 这条边是一个 M2M(多对多)关系,groups 这条件也是一个 M2M 的关系。

To 和 From

edge.Toedge.From 是两个用于创建边(关系)的构建器.

在一个关系中,使用 edge.To 定义边的模式,拥有该关系;而使用 edge.From 定义边的模式;只是反向引用了该关系。

继续看一些例子,这些例子示范了如何使用边定义不同的关系。

关系

两种类型的一对一关系

er-user-card

在这个例子中,一个用户只有 一张 信用卡,一张信用卡只能有 一个 户主。

User 模式中使用 edge.To 为信用卡定义一条名为 card 的边。并在 Card 模式中使用 edge.From 为用户定义一条名为 owner 的逆边。

ent/schema/user.go

  1. // Edges of the user.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("card", Card.Type).
  5. Unique(),
  6. }
  7. }

ent/schema/card.go

  1. // Edges of the user.
  2. func (Card) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.From("owner", User.Type).
  5. Ref("card").
  6. Unique().
  7. // 我们在构建器中添加 `Required` 方法。
  8. // 使得创建实体时也必须满足这条边,即:信用卡在创建时,不能没有户主。
  9. Required(),
  10. }
  11. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. a8m, err := client.User.
  3. Create().
  4. SetAge(30).
  5. SetName("Mashraki").
  6. Save(ctx)
  7. if err != nil {
  8. return fmt.Errorf("creating user: %v", err)
  9. }
  10. log.Println("user:", a8m)
  11. card1, err := client.Card.
  12. Create().
  13. SetOwner(a8m).
  14. SetNumber("1020").
  15. SetExpired(time.Now().Add(time.Minute)).
  16. Save(ctx)
  17. if err != nil {
  18. return fmt.Errorf("creating card: %v", err)
  19. }
  20. log.Println("card:", card1)
  21. // 返回满足条件的用户的信用卡,并且期望的数量是 1.
  22. card2, err := a8m.QueryCard().Only(ctx)
  23. if err != nil {
  24. return fmt.Errorf("querying card: %v", err)
  25. }
  26. log.Println("card:", card2)
  27. // 在信用卡实体中,可以通过反向引用查询其户主。
  28. owner, err := card2.QueryOwner().Only(ctx)
  29. if err != nil {
  30. return fmt.Errorf("querying owner: %v", err)
  31. }
  32. log.Println("owner:", owner)
  33. return nil
  34. }

完整的例子请查看 GitHub.

同类型的一对一关系

er-linked-list

在这个链表例子中,我们有一个名为 next/prev 的递归关系。链表中的每个节点都只有 一个 nextprev 节点。 如果,节点 A 可以通过 next 指向 节点 B,则节点 B 也可以通过 prev(反向引用) 指向节点 A。

ent/schema/node.go

  1. // Edges of the Node.
  2. func (Node) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("next", Node.Type).
  5. Unique().
  6. From("prev").
  7. Unique(),
  8. }
  9. }

如你所见,在同类型关系的情况下,可以在一个构建器内声明边及其引用。

  1. func (Node) Edges() []ent.Edge {
  2. return []ent.Edge{
  3. + edge.To("next", Node.Type).
  4. + Unique().
  5. + From("prev").
  6. + Unique(),
  7. - // 不必写两次。
  8. - edge.To("next", Node.Type).
  9. - Unique(),
  10. - edge.From("prev", Node.Type).
  11. - Ref("next).
  12. - Unique(),
  13. }
  14. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. head, err := client.Node.
  3. Create().
  4. SetValue(1).
  5. Save(ctx)
  6. if err != nil {
  7. return fmt.Errorf("creating the head: %v", err)
  8. }
  9. curr := head
  10. // 下面的代码会生成链表: 1<->2<->3<->4<->5.
  11. for i := 0; i < 4; i++ {
  12. curr, err = client.Node.
  13. Create().
  14. SetValue(curr.Value + 1).
  15. SetPrev(curr).
  16. Save(ctx)
  17. if err != nil {
  18. return err
  19. }
  20. }
  21. // 遍历并打印列表. 如果遇到错误 `FirstX` 会 panics.
  22. for curr = head; curr != nil; curr = curr.QueryNext().FirstX(ctx) {
  23. fmt.Printf("%d ", curr.Value)
  24. }
  25. // Output: 1 2 3 4 5
  26. // 构建循环链表:
  27. // 链表的最后一个元素,没有 "next".
  28. tail, err := client.Node.
  29. Query().
  30. Where(node.Not(node.HasNext())).
  31. Only(ctx)
  32. if err != nil {
  33. return fmt.Errorf("getting the tail of the list: %v", tail)
  34. }
  35. tail, err = tail.Update().SetNext(head).Save(ctx)
  36. if err != nil {
  37. return err
  38. }
  39. // 检查修改是否生效:
  40. prev, err := head.QueryPrev().Only(ctx)
  41. if err != nil {
  42. return fmt.Errorf("getting head's prev: %v", err)
  43. }
  44. fmt.Printf("\n%v", prev.Value == tail.Value)
  45. // Output: true
  46. return nil
  47. }

完整的例子请查看 GitHub.

双向的一对一关系

er-user-spouse

在这个用户-配偶例子中,我们有一个名为 spouse对称一对一 关系。每个用户只能有 一个配偶。 如果用户 A 的配偶是(使用 spouse 关系) 是 B,那么也可以得知 B 的配偶(使用 spouse 关系)。

注意,在双向关系中,不存在拥有/属于这种说法。

ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("spouse", User.Type).
  5. Unique(),
  6. }
  7. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. a8m, err := client.User.
  3. Create().
  4. SetAge(30).
  5. SetName("a8m").
  6. Save(ctx)
  7. if err != nil {
  8. return fmt.Errorf("creating user: %v", err)
  9. }
  10. nati, err := client.User.
  11. Create().
  12. SetAge(28).
  13. SetName("nati").
  14. SetSpouse(a8m).
  15. Save(ctx)
  16. if err != nil {
  17. return fmt.Errorf("creating user: %v", err)
  18. }
  19. // 查询名为 配偶 的边
  20. // 不同于 `Only`, `OnlyX`遇到错误会引起 panics.
  21. spouse := nati.QuerySpouse().OnlyX(ctx)
  22. fmt.Println(spouse.Name)
  23. // Output: a8m
  24. spouse = a8m.QuerySpouse().OnlyX(ctx)
  25. fmt.Println(spouse.Name)
  26. // Output: nati
  27. // 查询有配偶用户的数量。
  28. // 不同于 `Count`, `CountX`遇到错误会引起 panics.
  29. count := client.User.
  30. Query().
  31. Where(user.HasSpouse()).
  32. CountX(ctx)
  33. fmt.Println(count)
  34. // Output: 2
  35. // 获取有配偶,且其配偶姓名为 "a8m" 的用户。
  36. spouse = client.User.
  37. Query().
  38. Where(user.HasSpouseWith(user.Name("a8m"))).
  39. OnlyX(ctx)
  40. fmt.Println(spouse.Name)
  41. // Output: nati
  42. return nil
  43. }

完整的例子请查看 GitHub.

不同类型的一对多关系

er-user-pets

在这个 用户-宠物 的例子中,用户和宠物之间存在一个 O2M (一对多)关系。 每个用户可以有 多个 宠物,但是一个宠物只有 一个 主人(用户)。如果用户 A 通过 pets 边添加了一个宠物 B,那么,宠物 B 可以通过 owner 边(反向引用边)找到他的主人。

注意,从 Pet (宠物)的角度来说,这就是多对一的关系(M20,many-to-one)。

ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("pets", Pet.Type),
  5. }
  6. }

ent/schema/pet.go

  1. // Edges of the Pet.
  2. func (Pet) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.From("owner", User.Type).
  5. Ref("pets").
  6. Unique(),
  7. }
  8. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // 创建两个宠物。
  3. pedro, err := client.Pet.
  4. Create().
  5. SetName("pedro").
  6. Save(ctx)
  7. if err != nil {
  8. return fmt.Errorf("creating pet: %v", err)
  9. }
  10. lola, err := client.Pet.
  11. Create().
  12. SetName("lola").
  13. Save(ctx)
  14. if err != nil {
  15. return fmt.Errorf("creating pet: %v", err)
  16. }
  17. // 创建用户的同时给他添加两个宠物。
  18. a8m, err := client.User.
  19. Create().
  20. SetAge(30).
  21. SetName("a8m").
  22. AddPets(pedro, lola).
  23. Save(ctx)
  24. if err != nil {
  25. return fmt.Errorf("creating user: %v", err)
  26. }
  27. fmt.Println("User created:", a8m)
  28. // Output: User(id=1, age=30, name=a8m)
  29. // 查询主人,不同于 `Only`, `OnlyX` 遇到错误时会引起 panics.
  30. owner := pedro.QueryOwner().OnlyX(ctx)
  31. fmt.Println(owner.Name)
  32. // Output: a8m
  33. // 遍历子图,不同于 `Count`, `CountX` 遇到错误时会引起 panics.
  34. count := pedro.
  35. QueryOwner(). // a8m
  36. QueryPets(). // pedro, lola
  37. CountX(ctx) // count
  38. fmt.Println(count)
  39. // Output: 2
  40. return nil
  41. }

完整的例子请查看 GitHub.

同类型的一对多关系

er-tree

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

  1. // Edges of the Node.
  2. func (Node) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("children", Node.Type).
  5. From("parent").
  6. Unique(),
  7. }
  8. }

如你所见,在同类型关系的情况下,可以在一个构建器内声明边及其引用。

  1. func (Node) Edges() []ent.Edge {
  2. return []ent.Edge{
  3. + edge.To("children", Node.Type).
  4. + From("parent").
  5. + Unique(),
  6. - // 不必写两次。
  7. - edge.To("children", Node.Type),
  8. - edge.From("parent", Node.Type).
  9. - Ref("children").
  10. - Unique(),
  11. }
  12. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. root, err := client.Node.
  3. Create().
  4. SetValue(2).
  5. Save(ctx)
  6. if err != nil {
  7. return fmt.Errorf("creating the root: %v", err)
  8. }
  9. // 构建一颗这样的树:
  10. //
  11. // 2
  12. // / \
  13. // 1 4
  14. // / \
  15. // 3 5
  16. //
  17. // 不同于 `Create`, `CreateX` 遇到错误时会引起 panics.
  18. n1 := client.Node.
  19. Create().
  20. SetValue(1).
  21. SetParent(root).
  22. SaveX(ctx)
  23. n4 := client.Node.
  24. Create().
  25. SetValue(4).
  26. SetParent(root).
  27. SaveX(ctx)
  28. n3 := client.Node.
  29. Create().
  30. SetValue(3).
  31. SetParent(n4).
  32. SaveX(ctx)
  33. n5 := client.Node.
  34. Create().
  35. SetValue(5).
  36. SetParent(n4).
  37. SaveX(ctx)
  38. fmt.Println("Tree leafs", []int{n1.Value, n3.Value, n5.Value})
  39. // Output: Tree leafs [1 3 5]
  40. // 获取所有叶子节点(没有子节点的节点)。
  41. // Unlike `Int`, `IntX` panics if an error occurs.
  42. // 不同于 `Int`, `IntX` 遇到错误时会引起 panics.
  43. ints := client.Node.
  44. Query(). // 全部节点.
  45. Where(node.Not(node.HasChildren())). // 叶子节点.
  46. Order(ent.Asc(node.FieldValue)). // 根据 `value` 字段升序排序.
  47. GroupBy(node.FieldValue). // 仅提取 `value` 字段。
  48. IntsX(ctx)
  49. fmt.Println(ints)
  50. // Output: [1 3 5]
  51. // 获取孤儿节点(没有父节点的节点)。
  52. // Unlike `Only`, `OnlyX` panics if an error occurs.
  53. // 不用于 `Only`, `OnlyX` 遇到错误时会引起 panics.
  54. orphan := client.Node.
  55. Query().
  56. Where(node.Not(node.HasParent())).
  57. OnlyX(ctx)
  58. fmt.Println(orphan)
  59. // Output: Node(id=1, value=2)
  60. return nil
  61. }

完整的例子请查看 GitHub.

不同类型的多对多关系

er-user-groups

在这个例子中,在群组和用户之间存在一个多对多(M2M)的关系。 每个群主可以有 多个 用户,每个用户也可以加入 多个 群组。

ent/schema/group.go

  1. // Edges of the Group.
  2. func (Group) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("users", User.Type),
  5. }
  6. }

ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.From("groups", Group.Type).
  5. Ref("users"),
  6. }
  7. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // 不同于 `Save`, `SaveX` 遇到错误时会引起 panics.
  3. hub := client.Group.
  4. Create().
  5. SetName("GitHub").
  6. SaveX(ctx)
  7. lab := client.Group.
  8. Create().
  9. SetName("GitLab").
  10. SaveX(ctx)
  11. a8m := client.User.
  12. Create().
  13. SetAge(30).
  14. SetName("a8m").
  15. AddGroups(hub, lab).
  16. SaveX(ctx)
  17. nati := client.User.
  18. Create().
  19. SetAge(28).
  20. SetName("nati").
  21. AddGroups(hub).
  22. SaveX(ctx)
  23. // 关系查询
  24. groups, err := a8m.
  25. QueryGroups().
  26. All(ctx)
  27. if err != nil {
  28. return fmt.Errorf("querying a8m groups: %v", err)
  29. }
  30. fmt.Println(groups)
  31. // Output: [Group(id=1, name=GitHub) Group(id=2, name=GitLab)]
  32. groups, err = nati.
  33. QueryGroups().
  34. All(ctx)
  35. if err != nil {
  36. return fmt.Errorf("querying nati groups: %v", err)
  37. }
  38. fmt.Println(groups)
  39. // Output: [Group(id=1, name=GitHub)]
  40. // 图遍历
  41. users, err := a8m.
  42. QueryGroups(). // [hub, lab]
  43. Where(group.Not(group.HasUsersWith(user.Name("nati")))). // [lab]
  44. QueryUsers(). // [a8m]
  45. QueryGroups(). // [hub, lab]
  46. QueryUsers(). // [a8m, nati]
  47. All(ctx)
  48. if err != nil {
  49. return fmt.Errorf("traversing the graph: %v", err)
  50. }
  51. fmt.Println(users)
  52. // Output: [User(id=1, age=30, name=a8m) User(id=2, age=28, name=nati)]
  53. return nil
  54. }

完整的例子请查看 GitHub.

同类型的多对多关系

er-following-followers

下面这个 关注-粉丝 的例子,在用户及其粉丝之间存在一个多对多(M2M)的关系。 每个用户可以关注 多个 用户,也可以有 多个 粉丝。

ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("following", User.Type).
  5. From("followers"),
  6. }
  7. }

如你所见,在同类型关系的情况下,可以在一个构建器内声明边及其引用。

  1. func (User) Edges() []ent.Edge {
  2. return []ent.Edge{
  3. + edge.To("following", User.Type).
  4. + From("followers"),
  5. - // 不必写两次
  6. - edge.To("following", User.Type),
  7. - edge.From("followers", User.Type).
  8. - Ref("following"),
  9. }
  10. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // 不同于 `Save`, `SaveX` 遇到错误时会引起 panics.
  3. a8m := client.User.
  4. Create().
  5. SetAge(30).
  6. SetName("a8m").
  7. SaveX(ctx)
  8. nati := client.User.
  9. Create().
  10. SetAge(28).
  11. SetName("nati").
  12. AddFollowers(a8m).
  13. SaveX(ctx)
  14. // 查询关注/粉丝列表:
  15. flw := a8m.QueryFollowing().AllX(ctx)
  16. fmt.Println(flw)
  17. // Output: [User(id=2, age=28, name=nati)]
  18. flr := a8m.QueryFollowers().AllX(ctx)
  19. fmt.Println(flr)
  20. // Output: []
  21. flw = nati.QueryFollowing().AllX(ctx)
  22. fmt.Println(flw)
  23. // Output: []
  24. flr = nati.QueryFollowers().AllX(ctx)
  25. fmt.Println(flr)
  26. // Output: [User(id=1, age=30, name=a8m)]
  27. // 图遍历:
  28. ages := nati.
  29. QueryFollowers(). // [a8m]
  30. QueryFollowing(). // [nati]
  31. GroupBy(user.FieldAge). // [28]
  32. IntsX(ctx)
  33. fmt.Println(ages)
  34. // Output: [28]
  35. names := client.User.
  36. Query().
  37. Where(user.Not(user.HasFollowers())).
  38. GroupBy(user.FieldName).
  39. StringsX(ctx)
  40. fmt.Println(names)
  41. // Output: [a8m]
  42. return nil
  43. }

完整的例子请查看 GitHub.

双向的多对多关系

er-user-friends

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

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("friends", User.Type),
  5. }
  6. }

下面是一些与边交互的 API:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // 不同于 `Save`, `SaveX` 遇到错误时会引起 panics.
  3. a8m := client.User.
  4. Create().
  5. SetAge(30).
  6. SetName("a8m").
  7. SaveX(ctx)
  8. nati := client.User.
  9. Create().
  10. SetAge(28).
  11. SetName("nati").
  12. AddFriends(a8m).
  13. SaveX(ctx)
  14. // 查询朋友列表。不同于 `All`, `AllX` 遇到错误是会引起 panics.
  15. friends := nati.
  16. QueryFriends().
  17. AllX(ctx)
  18. fmt.Println(friends)
  19. // Output: [User(id=1, age=30, name=a8m)]
  20. friends = a8m.
  21. QueryFriends().
  22. AllX(ctx)
  23. fmt.Println(friends)
  24. // Output: [User(id=2, age=28, name=nati)]
  25. // 图遍历:
  26. friends = client.User.
  27. Query().
  28. Where(user.HasFriends()).
  29. AllX(ctx)
  30. fmt.Println(friends)
  31. // Output: [User(id=1, age=30, name=a8m) User(id=2, age=28, name=nati)]
  32. return nil
  33. }

完整的例子请查看 GitHub.

必选项

Edges can be defined as required in the entity creation using the Required method on the builder. 可以使用构建器中的 Required 方法定义关系,使得实体创建时必须满足该关系。

  1. // Edges of the user.
  2. func (Card) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.From("owner", User.Type).
  5. Ref("card").
  6. Unique().
  7. Required(),
  8. }
  9. }

比如说,无法创建一张没有户主的信用卡。

索引

可以在多个字段或者某些边上添加索引。但是,需要注意的是,目前只有 SQL 支持索引特性。

更多关于索引的内容,可以查阅 索引 部分。