Graph Traversal

For the purpose of the example, we’ll generate the following graph:

er-traversal-graph

The first step is to generate the 3 schemas: Pet, User, Group.

  1. go run -mod=mod entgo.io/ent/cmd/ent new Pet User Group

Add the necessary fields and edges for the schemas:

ent/schema/pet.go

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

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.Int("age"),
  9. field.String("name"),
  10. }
  11. }
  12. // Edges of the User.
  13. func (User) Edges() []ent.Edge {
  14. return []ent.Edge{
  15. edge.To("pets", Pet.Type),
  16. edge.To("friends", User.Type),
  17. edge.From("groups", Group.Type).
  18. Ref("users"),
  19. edge.From("manage", Group.Type).
  20. Ref("admin"),
  21. }
  22. }

ent/schema/group.go

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

Let’s write the code for populating the vertices and the edges to the graph:

  1. func Gen(ctx context.Context, client *ent.Client) error {
  2. hub, err := client.Group.
  3. Create().
  4. SetName("Github").
  5. Save(ctx)
  6. if err != nil {
  7. return fmt.Errorf("failed creating the group: %w", err)
  8. }
  9. // Create the admin of the group.
  10. // Unlike `Save`, `SaveX` panics if an error occurs.
  11. dan := client.User.
  12. Create().
  13. SetAge(29).
  14. SetName("Dan").
  15. AddManage(hub).
  16. SaveX(ctx)
  17. // Create "Ariel" and its pets.
  18. a8m := client.User.
  19. Create().
  20. SetAge(30).
  21. SetName("Ariel").
  22. AddGroups(hub).
  23. AddFriends(dan).
  24. SaveX(ctx)
  25. pedro := client.Pet.
  26. Create().
  27. SetName("Pedro").
  28. SetOwner(a8m).
  29. SaveX(ctx)
  30. xabi := client.Pet.
  31. Create().
  32. SetName("Xabi").
  33. SetOwner(a8m).
  34. SaveX(ctx)
  35. // Create "Alex" and its pets.
  36. alex := client.User.
  37. Create().
  38. SetAge(37).
  39. SetName("Alex").
  40. SaveX(ctx)
  41. coco := client.Pet.
  42. Create().
  43. SetName("Coco").
  44. SetOwner(alex).
  45. AddFriends(pedro).
  46. SaveX(ctx)
  47. fmt.Println("Pets created:", pedro, xabi, coco)
  48. // Output:
  49. // Pets created: Pet(id=1, name=Pedro) Pet(id=2, name=Xabi) Pet(id=3, name=Coco)
  50. return nil
  51. }

Let’s go over a few traversals, and show the code for them:

er-traversal-graph-gopher

The traversal above starts from a Group entity, continues to its admin (edge), continues to its friends (edge), gets their pets (edge), gets each pet’s friends (edge), and requests their owners.

  1. func Traverse(ctx context.Context, client *ent.Client) error {
  2. owner, err := client.Group. // GroupClient.
  3. Query(). // Query builder.
  4. Where(group.Name("Github")). // Filter only Github group (only 1).
  5. QueryAdmin(). // Getting Dan.
  6. QueryFriends(). // Getting Dan's friends: [Ariel].
  7. QueryPets(). // Their pets: [Pedro, Xabi].
  8. QueryFriends(). // Pedro's friends: [Coco], Xabi's friends: [].
  9. QueryOwner(). // Coco's owner: Alex.
  10. Only(ctx) // Expect only one entity to return in the query.
  11. if err != nil {
  12. return fmt.Errorf("failed querying the owner: %w", err)
  13. }
  14. fmt.Println(owner)
  15. // Output:
  16. // User(id=3, age=37, name=Alex)
  17. return nil
  18. }

What about the following traversal?

er-traversal-graph-gopher-query

We want to get all pets (entities) that have an owner (edge) that is a friend (edge) of some group admin (edge).

  1. func Traverse(ctx context.Context, client *ent.Client) error {
  2. pets, err := client.Pet.
  3. Query().
  4. Where(
  5. pet.HasOwnerWith(
  6. user.HasFriendsWith(
  7. user.HasManage(),
  8. ),
  9. ),
  10. ).
  11. All(ctx)
  12. if err != nil {
  13. return fmt.Errorf("failed querying the pets: %w", err)
  14. }
  15. fmt.Println(pets)
  16. // Output:
  17. // [Pet(id=1, name=Pedro) Pet(id=2, name=Xabi)]
  18. return nil
  19. }

The full example exists in GitHub.