Edges

Quick Summary

Edges are the relations (or associations) of entities. For example, user’s pets, or group’s users:

  • Graph
  • ERD and SQL

er-group-users

erd-group-users

ERD was generated by Atlas

In the example above, you can see 2 relations declared using edges. Let’s go over them.

  1. pets / owner edges; user’s pets and pet’s owner:
  • User
  • Pet

ent/schema/user.go

  1. package schema
  2. import (
  3. "entgo.io/ent"
  4. "entgo.io/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. "entgo.io/ent"
  4. "entgo.io/ent/schema/edge"
  5. )
  6. // Pet holds the schema definition for the Pet entity.
  7. type Pet struct {
  8. ent.Schema
  9. }
  10. // Fields of the Pet.
  11. func (Pet) Fields() []ent.Field {
  12. return []ent.Field{
  13. // ...
  14. }
  15. }
  16. // Edges of the Pet.
  17. func (Pet) Edges() []ent.Edge {
  18. return []ent.Edge{
  19. edge.From("owner", User.Type).
  20. Ref("pets").
  21. Unique(),
  22. }
  23. }

As you can see, a User entity can have many pets, but a Pet entity can have only one owner.
In relationship definition, the pets edge is a O2M (one-to-many) relationship, and the owner edge is a M2O (many-to-one) relationship.

The User schema owns the pets/owner relationship because it uses edge.To, and the Pet schema just has a back-reference to it, declared using edge.From with the Ref method.

The Ref method describes which edge of the User schema we’re referencing because there can be multiple references from one schema to other.

The cardinality of the edge/relationship can be controlled using the Unique method, and it’s explained more widely below.

  1. users / groups edges; group’s users and user’s groups:
  • Group
  • User

ent/schema/group.go

  1. package schema
  2. import (
  3. "entgo.io/ent"
  4. "entgo.io/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. "entgo.io/ent"
  4. "entgo.io/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. }

As you can see, a Group entity can have many users, and a User entity can have have many groups.
In relationship definition, the users edge is a M2M (many-to-many) relationship, and the groups edge is also a M2M (many-to-many) relationship.

To and From

edge.To and edge.From are the 2 builders for creating edges/relations.

A schema that defines an edge using the edge.To builder owns the relation, unlike using the edge.From builder that gives only a back-reference for the relation (with a different name).

Let’s go over a few examples that show how to define different relation types using edges.

Relationship

O2O Two Types

  • Graph
  • ERD and SQL

er-user-card

edges-o2o-two-types

ERD was generated by Atlas

In this example, a user has only one credit-card, and a card has only one owner.

The User schema defines an edge.To card named card, and the Card schema defines a back-reference to this edge using edge.From named owner.

  • User
  • Card

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 Card.
  2. func (Card) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.From("owner", User.Type).
  5. Ref("card").
  6. Unique().
  7. // We add the "Required" method to the builder
  8. // to make this edge required on entity creation.
  9. // i.e. Card cannot be created without its owner.
  10. Required(),
  11. }
  12. }

The API for interacting with these edges is as follows:

  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: %w", 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: %w", err)
  19. }
  20. log.Println("card:", card1)
  21. // Only returns the card of the user,
  22. // and expects that there's only one.
  23. card2, err := a8m.QueryCard().Only(ctx)
  24. if err != nil {
  25. return fmt.Errorf("querying card: %w", err)
  26. }
  27. log.Println("card:", card2)
  28. // The Card entity is able to query its owner using
  29. // its back-reference.
  30. owner, err := card2.QueryOwner().Only(ctx)
  31. if err != nil {
  32. return fmt.Errorf("querying owner: %w", err)
  33. }
  34. log.Println("owner:", owner)
  35. return nil
  36. }

The full example exists in GitHub.

O2O Same Type

  • Graph
  • ERD and SQL

er-linked-list

edges-linked-list

ERD was generated by Atlas

In this linked-list example, we have a recursive relation named next/prev. Each node in the list can have only one next node. If a node A points (using next) to node B, B can get its pointer using prev (the back-reference edge).

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. }

As you can see, in cases of relations of the same type, you can declare the edge and its reference in the same builder.

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

The API for interacting with these edges is as follows:

  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: %w", err)
  8. }
  9. curr := head
  10. // Generate the following linked-list: 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. // Loop over the list and print it. `FirstX` panics if an error occur.
  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. // Make the linked-list circular:
  27. // The tail of the list, has no "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. // Check that the change actually applied:
  40. prev, err := head.QueryPrev().Only(ctx)
  41. if err != nil {
  42. return fmt.Errorf("getting head's prev: %w", err)
  43. }
  44. fmt.Printf("\n%v", prev.Value == tail.Value)
  45. // Output: true
  46. return nil
  47. }

The full example exists in GitHub.

O2O Bidirectional

  • Graph
  • ERD and SQL

er-user-spouse

edges-o2o-bidirectional

ERD was generated by Atlas

In this user-spouse example, we have a symmetric O2O relation named spouse. Each user can have only one spouse. If user A sets its spouse (using spouse) to B, B can get its spouse using the spouse edge.

Note that there are no owner/inverse terms in cases of bidirectional edges.

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. }

The API for interacting with this edge is as follows:

  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: %w", 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: %w", err)
  18. }
  19. // Query the spouse edge.
  20. // Unlike `Only`, `OnlyX` panics if an error occurs.
  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. // Query how many users have a spouse.
  28. // Unlike `Count`, `CountX` panics if an error occurs.
  29. count := client.User.
  30. Query().
  31. Where(user.HasSpouse()).
  32. CountX(ctx)
  33. fmt.Println(count)
  34. // Output: 2
  35. // Get the user, that has a spouse with name="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. }

Note that, the foreign-key column can be configured and exposed as an entity field using the Edge Field option as follows:

  1. // Fields of the User.
  2. func (User) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.Int("spouse_id").
  5. Optional(),
  6. }
  7. }
  8. // Edges of the User.
  9. func (User) Edges() []ent.Edge {
  10. return []ent.Edge{
  11. edge.To("spouse", User.Type).
  12. Unique().
  13. Field("spouse_id"),
  14. }
  15. }

The full example exists in GitHub.

O2M Two Types

  • Graph
  • ERD and SQL

er-user-pets

edges-o2m-two-types

ERD was generated by Atlas

In this user-pets example, we have a O2M relation between user and its pets. Each user has many pets, and a pet has one owner. If user A adds a pet B using the pets edge, B can get its owner using the owner edge (the back-reference edge).

Note that this relation is also a M2O (many-to-one) from the point of view of the Pet schema.

  • User
  • Pet

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. }

The API for interacting with these edges is as follows:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // Create the 2 pets.
  3. pedro, err := client.Pet.
  4. Create().
  5. SetName("pedro").
  6. Save(ctx)
  7. if err != nil {
  8. return fmt.Errorf("creating pet: %w", 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: %w", err)
  16. }
  17. // Create the user, and add its pets on the creation.
  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: %w", err)
  26. }
  27. fmt.Println("User created:", a8m)
  28. // Output: User(id=1, age=30, name=a8m)
  29. // Query the owner. Unlike `Only`, `OnlyX` panics if an error occurs.
  30. owner := pedro.QueryOwner().OnlyX(ctx)
  31. fmt.Println(owner.Name)
  32. // Output: a8m
  33. // Traverse the sub-graph. Unlike `Count`, `CountX` panics if an error occurs.
  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. }

Note that, the foreign-key column can be configured and exposed as an entity field using the Edge Field option as follows:

ent/schema/pet.go

  1. // Fields of the Pet.
  2. func (Pet) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.Int("owner_id").
  5. Optional(),
  6. }
  7. }
  8. // Edges of the Pet.
  9. func (Pet) Edges() []ent.Edge {
  10. return []ent.Edge{
  11. edge.From("owner", User.Type).
  12. Ref("pets").
  13. Unique().
  14. Field("owner_id"),
  15. }
  16. }

The full example exists in GitHub.

O2M Same Type

  • Graph
  • ERD and SQL

er-tree

edges-o2m-same-type

ERD was generated by Atlas

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.

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. }

As you can see, in cases of relations of the same type, you can declare the edge and its reference in the same builder.

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

The API for interacting with these edges is as follows:

  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: %w", err)
  8. }
  9. // Add additional nodes to the tree:
  10. //
  11. // 2
  12. // / \
  13. // 1 4
  14. // / \
  15. // 3 5
  16. //
  17. // Unlike `Save`, `SaveX` panics if an error occurs.
  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. // Get all leafs (nodes without children).
  41. // Unlike `Int`, `IntX` panics if an error occurs.
  42. ints := client.Node.
  43. Query(). // All nodes.
  44. Where(node.Not(node.HasChildren())). // Only leafs.
  45. Order(ent.Asc(node.FieldValue)). // Order by their `value` field.
  46. GroupBy(node.FieldValue). // Extract only the `value` field.
  47. IntsX(ctx)
  48. fmt.Println(ints)
  49. // Output: [1 3 5]
  50. // Get orphan nodes (nodes without parent).
  51. // Unlike `Only`, `OnlyX` panics if an error occurs.
  52. orphan := client.Node.
  53. Query().
  54. Where(node.Not(node.HasParent())).
  55. OnlyX(ctx)
  56. fmt.Println(orphan)
  57. // Output: Node(id=1, value=2)
  58. return nil
  59. }

Note that, the foreign-key column can be configured and exposed as an entity field using the Edge Field option as follows:

  1. // Fields of the Node.
  2. func (Node) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.Int("parent_id").
  5. Optional(),
  6. }
  7. }
  8. // Edges of the Node.
  9. func (Node) Edges() []ent.Edge {
  10. return []ent.Edge{
  11. edge.To("children", Node.Type).
  12. From("parent").
  13. Unique().
  14. Field("parent_id"),
  15. }
  16. }

The full example exists in GitHub.

M2M Two Types

  • Graph
  • ERD and SQL

er-user-groups

edges-m2m-two-types

ERD was generated by Atlas

In this groups-users example, we have a M2M relation between groups and their users. Each group has many users, and each user can be joined to many groups.

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. }

The API for interacting with these edges is as follows:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // Unlike `Save`, `SaveX` panics if an error occurs.
  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. // Query the edges.
  24. groups, err := a8m.
  25. QueryGroups().
  26. All(ctx)
  27. if err != nil {
  28. return fmt.Errorf("querying a8m groups: %w", 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: %w", err)
  37. }
  38. fmt.Println(groups)
  39. // Output: [Group(id=1, name=GitHub)]
  40. // Traverse the graph.
  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: %w", 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. }

Edges - 图15Calling AddGroups (a M2M edge) will result in a no-op in case the edge already exists and is not an EdgeSchema:

  1. a8m := client.User.
  2. Create().
  3. SetName("a8m").
  4. AddGroups(
  5. hub,
  6. hub, // no-op.
  7. ).
  8. SaveX(ctx)

The full example exists in GitHub.

M2M Same Type

  • Graph
  • ERD and SQL

er-following-followers

edges-m2m-same-type

ERD was generated by Atlas

In this following-followers example, we have a M2M relation between users to their followers. Each user can follow many users, and can have many followers.

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. }

As you can see, in cases of relations of the same type, you can declare the edge and its reference in the same builder.

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

The API for interacting with these edges is as follows:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // Unlike `Save`, `SaveX` panics if an error occurs.
  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. // Query following/followers:
  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. // Traverse the graph:
  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. }

Edges - 图18Calling AddFollowers (a M2M edge) will result in a no-op in case the edge already exists and is not an EdgeSchema:

  1. a8m := client.User.
  2. Create().
  3. SetName("a8m").
  4. AddFollowers(
  5. nati,
  6. nati, // no-op.
  7. ).
  8. SaveX(ctx)

The full example exists in GitHub.

M2M Bidirectional

  • Graph
  • ERD and SQL

er-user-friends

edges-m2m-bidirectional

ERD was generated by Atlas

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.

Note that there are no owner/inverse terms in cases of bidirectional edges.

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. }

The API for interacting with these edges is as follows:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. // Unlike `Save`, `SaveX` panics if an error occurs.
  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. // Query friends. Unlike `All`, `AllX` panics if an error occurs.
  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. // Query the graph:
  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. }

Edges - 图21Calling AddFriends (a M2M bidirectional edge) will result in a no-op in case the edge already exists and is not an EdgeSchema:

  1. a8m := client.User.
  2. Create().
  3. SetName("a8m").
  4. AddFriends(
  5. nati,
  6. nati, // no-op.
  7. ).
  8. SaveX(ctx)

The full example exists in GitHub.

Edge Field

The Field option for edges allows users to expose foreign-keys as regular fields on the schema. Note that only relations that hold foreign-keys (edge-ids) are allowed to use this option.

ent/schema/post.go

  1. // Fields of the Post.
  2. func (Post) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.Int("author_id").
  5. Optional(),
  6. }
  7. }
  8. // Edges of the Post.
  9. func (Post) Edges() []ent.Edge {
  10. return []ent.Edge{
  11. edge.To("author", User.Type).
  12. // Bind the "author_id" field to this edge.
  13. Field("author_id").
  14. Unique(),
  15. }
  16. }

The API for interacting with edge-fields is as follows:

  1. func Do(ctx context.Context, client *ent.Client) error {
  2. p, err := c.Post.Query().
  3. Where(post.AuthorID(id)).
  4. OnlyX(ctx)
  5. if err != nil {
  6. log.Fatal(err)
  7. }
  8. fmt.Println(p.AuthorID) // Access the "author" foreign-key.
  9. }

Multiple examples exists in GitHub.

Migration To Edge Fields

As mentioned in the StorageKey section, Ent configures edge storage-keys (e.g. foreign-keys) by the edge.To. Therefore, if you want to add a field to an existing edge (already exists in the database as a column), you need to set it up with the StorageKey option as follows:

  1. // Fields of the Post.
  2. func (Post) Fields() []ent.Field {
  3. return []ent.Field{
  4. + field.Int("author_id").
  5. + Optional(),
  6. }
  7. }
  8. // Edges of the Post.
  9. func (Post) Edges() []ent.Edge {
  10. return []ent.Edge{
  11. edge.From("author", User.Type).
  12. + Field("author_id").
  13. + StorageKey(edge.Column("post_author")).
  14. Unique(),
  15. }
  16. }

Alternatively, this option can be configured on the edge-field instead:

  1. // Fields of the Post.
  2. func (Post) Fields() []ent.Field {
  3. return []ent.Field{
  4. + field.Int("author_id").
  5. + StorageKey("post_author").
  6. + Optional(),
  7. }
  8. }

If you’re not sure how the foreign-key was named before using the edge-field option, check out the generated schema description in your project: <project>/ent/migrate/schema.go.

Edge Schema

Edge schemas are intermediate entity schemas for M2M edges. By using the Through option, users can define edge schemas for relationships. This allows users to expose relationships in their public APIs, store additional fields, apply CRUD operations, and set hooks and privacy policies on edges.

User Friendships Example

In the following example, we demonstrate how to model the friendship between two users using an edge schema with the two required fields of the relationship (user_id and friend_id), and an additional field named created_at whose value is automatically set on creation.

  • Graph
  • ERD and SQL

er_edgeschema_bidi

edges-schema

ERD was generated by Atlas

  • User
  • Friendship

ent/schema/user.go

  1. // User holds the schema definition for the User entity.
  2. type User struct {
  3. ent.Schema
  4. }
  5. // Fields of the User.
  6. func (User) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.String("name").
  9. Default("Unknown"),
  10. }
  11. }
  12. // Edges of the User.
  13. func (User) Edges() []ent.Edge {
  14. return []ent.Edge{
  15. edge.To("friends", User.Type).
  16. Through("friendships", Friendship.Type),
  17. }
  18. }

ent/schema/friendship.go

  1. // Friendship holds the edge schema definition of the Friendship relationship.
  2. type Friendship struct {
  3. ent.Schema
  4. }
  5. // Fields of the Friendship.
  6. func (Friendship) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.Time("created_at").
  9. Default(time.Now),
  10. field.Int("user_id"),
  11. field.Int("friend_id"),
  12. }
  13. }
  14. // Edges of the Friendship.
  15. func (Friendship) Edges() []ent.Edge {
  16. return []ent.Edge{
  17. edge.To("user", User.Type).
  18. Required().
  19. Unique().
  20. Field("user_id"),
  21. edge.To("friend", User.Type).
  22. Required().
  23. Unique().
  24. Field("friend_id"),
  25. }
  26. }

Edges - 图24info

  • Similar to entity schemas, the ID field is automatically generated for edge schemas if not stated otherwise.
  • Edge schemas cannot be used by more than one relationship.
  • The user_id and friend_id edge-fields are required in the edge schema as they compose the relationship. :::

User Likes Example

In the following example, we demonstrate how to model a system where users can “like” tweets, and a timestamp of when the tweet was “liked” is stored in the database. This is a way to store additional fields on the edge.

  • User
  • Tweet
  • Like

ent/schema/user.go

  1. // User holds the schema definition for the User entity.
  2. type User struct {
  3. ent.Schema
  4. }
  5. // Fields of the User.
  6. func (User) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.String("name").
  9. Default("Unknown"),
  10. }
  11. }
  12. // Edges of the User.
  13. func (User) Edges() []ent.Edge {
  14. return []ent.Edge{
  15. edge.To("liked_tweets", Tweet.Type).
  16. Through("likes", Like.Type),
  17. }
  18. }

ent/schema/tweet.go

  1. // Tweet holds the schema definition for the Tweet entity.
  2. type Tweet struct {
  3. ent.Schema
  4. }
  5. // Fields of the Tweet.
  6. func (Tweet) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.Text("text"),
  9. }
  10. }
  11. // Edges of the Tweet.
  12. func (Tweet) Edges() []ent.Edge {
  13. return []ent.Edge{
  14. edge.From("liked_users", User.Type).
  15. Ref("liked_tweets").
  16. Through("likes", Like.Type),
  17. }
  18. }

ent/schema/like.go

  1. // Like holds the edge schema definition for the Like edge.
  2. type Like struct {
  3. ent.Schema
  4. }
  5. func (Like) Annotations() []schema.Annotation {
  6. return []schema.Annotation{
  7. field.ID("user_id", "tweet_id"),
  8. }
  9. }
  10. // Fields of the Like.
  11. func (Like) Fields() []ent.Field {
  12. return []ent.Field{
  13. field.Time("liked_at").
  14. Default(time.Now),
  15. field.Int("user_id"),
  16. field.Int("tweet_id"),
  17. }
  18. }
  19. // Edges of the Like.
  20. func (Like) Edges() []ent.Edge {
  21. return []ent.Edge{
  22. edge.To("user", User.Type).
  23. Unique().
  24. Required().
  25. Field("user_id"),
  26. edge.To("tweet", Tweet.Type).
  27. Unique().
  28. Required().
  29. Field("tweet_id"),
  30. }
  31. }

Usage Of Edge Schema In Other Edge Types

In some cases, users want to store O2M/M2O or O2O relationships in a separate table (i.e. join table) in order to simplify future migrations in case the edge type was changed. For example, wanting to change a O2M/M2O edge to M2M by dropping a unique constraint instead of migrating foreign-key values to a new table.

In the following example, we present a model where users can “author” tweets with the constraint that a tweet can be written by only one user. Unlike regular O2M/M2O edges, by using an edge schema, we enforce this constraint on the join table using a unique index on the tweet_id column. This constraint may be dropped in the future to allow multiple users to participate in the “authoring” of a tweet. Hence, changing the edge type to M2M without migrating the data to a new table.

  • User
  • Tweet
  • UserTweet

ent/schema/user.go

  1. // User holds the schema definition for the User entity.
  2. type User struct {
  3. ent.Schema
  4. }
  5. // Fields of the User.
  6. func (User) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.String("name").
  9. Default("Unknown"),
  10. }
  11. }
  12. // Edges of the User.
  13. func (User) Edges() []ent.Edge {
  14. return []ent.Edge{
  15. edge.To("tweets", Tweet.Type).
  16. Through("user_tweets", UserTweet.Type),
  17. }
  18. }

ent/schema/tweet.go

  1. // Tweet holds the schema definition for the Tweet entity.
  2. type Tweet struct {
  3. ent.Schema
  4. }
  5. // Fields of the Tweet.
  6. func (Tweet) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.Text("text"),
  9. }
  10. }
  11. // Edges of the Tweet.
  12. func (Tweet) Edges() []ent.Edge {
  13. return []ent.Edge{
  14. edge.From("user", User.Type).
  15. Ref("tweets").
  16. Through("tweet_user", UserTweet.Type).
  17. Comment("The uniqueness of the author is enforced on the edge schema"),
  18. }
  19. }

ent/schema/usertweet.go

  1. // UserTweet holds the schema definition for the UserTweet entity.
  2. type UserTweet struct {
  3. ent.Schema
  4. }
  5. // Fields of the UserTweet.
  6. func (UserTweet) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.Time("created_at").
  9. Default(time.Now),
  10. field.Int("user_id"),
  11. field.Int("tweet_id"),
  12. }
  13. }
  14. // Edges of the UserTweet.
  15. func (UserTweet) Edges() []ent.Edge {
  16. return []ent.Edge{
  17. edge.To("user", User.Type).
  18. Unique().
  19. Required().
  20. Field("user_id"),
  21. edge.To("tweet", Tweet.Type).
  22. Unique().
  23. Required().
  24. Field("tweet_id"),
  25. }
  26. }
  27. // Indexes of the UserTweet.
  28. func (UserTweet) Indexes() []ent.Index {
  29. return []ent.Index{
  30. index.Fields("tweet_id").
  31. Unique(),
  32. }
  33. }

Required

Edges can be defined as required in the entity creation using the Required method on the builder.

  1. // Edges of the Card.
  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. }

If the example above, a card entity cannot be created without its owner.

Edges - 图25Note that, starting with v0.10, foreign key columns are created as NOT NULL in the database for required edges that are not self-reference. In order to migrate existing foreign key columns, use the Atlas Migration option. :::

Immutable

Immutable edges are edges that can be set or added only in the creation of the entity. i.e., no setters will be generated for the update builders of the entity.

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("tenant", Tenant.Type).
  5. Field("tenant_id").
  6. Unique().
  7. Required().
  8. Immutable(),
  9. }
  10. }

StorageKey

By default, Ent configures edge storage-keys by the edge-owner (the schema that holds the edge.To), and not the by back-reference (edge.From). This is because back-references are optional and can be removed.

In order to use custom storage configuration for edges, use the StorageKey method as follows:

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("pets", Pet.Type).
  5. // Set the column name in the "pets" table for O2M relationship.
  6. StorageKey(edge.Column("owner_id")),
  7. edge.To("cars", Car.Type).
  8. // Set the symbol of the foreign-key constraint for O2M relationship.
  9. StorageKey(edge.Symbol("cars_owner_id")),
  10. edge.To("friends", User.Type).
  11. // Set the join-table, and the column names for a M2M relationship.
  12. StorageKey(edge.Table("friends"), edge.Columns("user_id", "friend_id")),
  13. edge.To("groups", Group.Type).
  14. // Set the join-table, its column names and the symbols
  15. // of the foreign-key constraints for M2M relationship.
  16. StorageKey(
  17. edge.Table("groups"),
  18. edge.Columns("user_id", "group_id"),
  19. edge.Symbols("groups_id1", "groups_id2")
  20. ),
  21. }
  22. }

Struct Tags

Custom struct tags can be added to the generated entities using the StructTag method. Note that if this option was not provided, or provided and did not contain the json tag, the default json tag will be added with the field name.

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("pets", Pet.Type).
  5. // Override the default json tag "pets" with "owner" for O2M relationship.
  6. StructTag(`json:"owner"`),
  7. }
  8. }

Indexes

Indexes can be defined on multi fields and some types of edges as well. However, you should note, that this is currently an SQL-only feature.

Read more about this in the Indexes section.

Comments

A comment can be added to the edge using the .Comment() method. This comment appears before the edge in the generated entity code. Newlines are supported using the \n escape sequence.

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("pets", Pet.Type).
  5. Comment("Pets that this user is responsible for taking care of.\n" +
  6. "May be zero to many, depending on the user.")
  7. }
  8. }

Annotations

Annotations is used to attach arbitrary metadata to the edge object in code generation. Template extensions can retrieve this metadata and use it inside their templates.

Note that the metadata object must be serializable to a JSON raw value (e.g. struct, map or slice).

  1. // Pet schema.
  2. type Pet struct {
  3. ent.Schema
  4. }
  5. // Edges of the Pet.
  6. func (Pet) Edges() []ent.Edge {
  7. return []ent.Edge{
  8. edge.To("owner", User.Type).
  9. Ref("pets").
  10. Unique().
  11. Annotations(entgql.RelayConnection()),
  12. }
  13. }

Read more about annotations and their usage in templates in the template doc.

Naming Convention

By convention edge names should use snake_case. The corresponding struct fields generated by ent will follow the Go convention of using PascalCase. In cases where PascalCase is desired, you can do so with the StorageKey or StructTag methods.