GraphQL many-to-many self-referential relations

This tutorial assumes that you have successfully completed the Dashboard quick start tutorial, and that you still have the Fauna Dashboard open in a browser tab/window. Create a new database before you start the tutorial.

If your Dashboard session has expired:

  1. Log in again.

  2. Select the database.

  3. Click the GRAPHQL button in the left navigation.

To form bi-directional relations in GraphQL requires using the @relation directive, which is explained in the GraphQL relations.

In this tutorial, we explore how to form many-to-many relations from a GraphQL Type to itself. We are using a generic Person Type as an example of modeling parent/child relations. That is, every Person document can have multiple parent and multiple children relations, where a parent and child are also of the same Person Type. The same can be applied to any use case where many-to-many self-referential relation holds, such as where User can have any number of Followers (child relation) and each Follower is also a User that can follow multiple other Users (parent relation).

To create such self-referential relations, the relations in GraphQL are modeled using an intermediate "link table" Type.

Tutorial

Let’s first define a simple Person Type, and attempt to make a relation to itself.

  1. Create a new schema file

    Create the file schema-self_ref.graphql with the following content:

    type Person {
      name: String!
      children: [Person]! @relation
    }
  2. Import the GraphQL schema

    1. Click the IMPORT SCHEMA button. If you do not see the button, click the REPLACE SCHEMA button.

    2. In the file dialog, locate and select the file schema-self_ref.graphql, then click the file dialog’s Open button.

    An error message appears: Many to many self-references are not allowed

    The problem is that the GraphQL API does not support applying the dynamically-generated document ID to a document field as a single step, which is required to make a self-referential relation.

    To solve this problem, the schema needs to be adjusted to introduce a Type dedicated to managing the relation.

  3. Modify the schema to create a PersonLink Type to establish a self-referencing many-to-many relation

    Create the file schema-self-many-many-relations.gql with the following content (or download it here):

    type Person {
      name: String!
      children: [PersonLink]! @relation(name: "person_children")
      parents: [PersonLink]! @relation(name: "person_parents")
    }
    
    type PersonLink {
      parent: Person! @relation(name: "person_children")
      child: Person! @relation(name: "person_parents")
    }
  4. Replace the schema

    1. Click the REPLACE SCHEMA button.

    2. In the file dialog, locate and select the file schema-self_ref.graphql, then click the file dialog’s Open button.

    This time, the schema import should be successful.

    When the schema is imported, the Fauna GraphQL API creates collections named Person and PersonLink. It specifies that a Person document can have multiple parent documents and multiple child documents.

  5. Create Person and it’s parent-child relations

    Now that you have the new schema in place, to create relationships between the parent/child persons, you have to create the "links".

    1. Copy the following GraphQL mutation, which creates a Person document named Person1, plus its children documents Child1, Child2, and Child3 and its parents Parent1 and Parent2:

      mutation{
        createPerson(data: {
          name: "Person1",
          children: {
            create: [
              { child: { create: { name: "Child1"}}},
              { child: { create: { name: "Child2"}}},
              { child: { create: { name: "Child3"}}}
            ]
          }
          parents: {
            create: [
              { parent: { create: { name: "Parent1"}}},
              { parent: { create: { name: "Parent2"}}}
            ]
          }
        })
        {
          _id
          name
          children {
            data {
              _id
              child {
                _id
                name
              }
            }
          }
          parents {
            data {
              _id
              parent {
                _id
                name
              }
            }
          }
        }
      }
    2. Click the "new tab" + button in the GraphQL Playground screen in your browser (at the top left, just right of the last query tab).

    3. Paste the query into the left panel, and click the "Play" button.

    The query should execute and it should create a list of PersonLink documents, each one creating a Person as a child and a Person as a parent. The response should appear in the panel on the right:

    {
      "data": {
        "createPerson": {
          "_id": "335277188298310144",
          "name": "Person1",
          "children": {
            "data": [
              {
                "_id": "335277188302504448",
                "child": {
                  "_id": "335277188301455872",
                  "name": "Child1"
                }
              },
              {
                "_id": "335277188302506496",
                "child": {
                  "_id": "335277188302505472",
                  "name": "Child2"
                }
              },
              {
                "_id": "335277188303554048",
                "child": {
                  "_id": "335277188303553024",
                  "name": "Child3"
                }
              }
            ]
          },
          "parents": {
            "data": [
              {
                "_id": "335277188304602624",
                "parent": {
                  "_id": "335277188304601600",
                  "name": "Parent1"
                }
              },
              {
                "_id": "335277188305651200",
                "parent": {
                  "_id": "335277188305650176",
                  "name": "Parent2"
                }
              }
            ]
          }
        }
      }
    }

    Alternately, you can create Person documents and then create a link to connect parent and child. For example, execute the following GraphQL queries in sequence, as you have executed the previous queries:

    mutation{
      createPerson(data: {
        name: "Person1"
      })
      {
        _id
        name
      }
    }
    mutation {
      createPerson(data: {
        name: "Child1"
      })
      {
        _id
        name
      }
    }
    {
      "data": {
        "createPerson": {
          "_id": "332505815169630400",
          "name": "Child1"
        }
      }
    }

    For the next query, copy the _id value for Parent1 and replace the <$PARENT_ID> string with that value, and copy the _id for Child1 and replace the <$CHILD_ID> string with that value.

    mutation {
      createPersonLink(data: {
        parent: { connect: "<$PARENT_ID>"}
        child: { connect: "<$CHILD_ID>"}
      })
      {
        _id
        parent{
          _id
          name
        }
        child{
          _id
          name
        }
      }
    }
    {
      "data": {
        "createPersonLink": {
          "_id": "335277379989537280",
          "parent": {
            "_id": "335277379887825408",
            "name": "Person1"
          },
          "child": {
            "_id": "335277379939205632",
            "name": "Child1"
          }
        }
      }
    }
  6. Create unique index

    To ensure that the same link is not made more than once, manually create a unique compound index on the joining fields. This index is not used directly, but as a unique index, it prevents duplicate entries from being created.

    try
    {
        Value result = await client.Query(
            CreateIndex(
                Obj(
                    "name", "unique_personLink_parent_child",
                    "source", Collection("PersonLink"),
                    "unique", true,
                    "terms", Arr(
                        Obj("field", Arr("data", "parent")),
                        Obj("field", Arr("data", "child"))
                    )
                )
            )
        );
        Console.WriteLine(result);
    }
    catch (Exception e)
    {
        Console.WriteLine($"ERROR: {e.Message}");
    }
    ObjectV(ref: RefV(id = "unique_personLink_parent_child", collection = RefV(id = "indexes")),ts: LongV(1651094261520000),active: BooleanV(True),serialized: BooleanV(True),unique: BooleanV(True),name: StringV(unique_personLink_parent_child),source: RefV(id = "PersonLink", collection = RefV(id = "collections")),terms: Arr(ObjectV(field: Arr(StringV(data), StringV(parent))), ObjectV(field: Arr(StringV(data), StringV(child)))), partitions: LongV(1))
    result, err := client.Query(
    	f.CreateIndex(
    		f.Obj{
    			"name": "unique_personLink_parent_child",
    			"source": f.Collection("PersonLink"),
    			"unique": true,
    			"terms": f.Arr{
    				f.Obj{"field": f.Arr{"data", "parent"}},
    				f.Obj{"field": f.Arr{"data", "child"}},
    			},
    		},
    	),
    )
    
    if err != nil {
    	fmt.Fprintln(os.Stderr, err)
    } else {
    	fmt.Println(result)
    }
    map[active:true name:unique_personLink_parent_child partitions:1 ref:{unique_personLink_parent_child 0x14000194240 0x14000194240 <nil>} serialized:true source:{PersonLink 0x14000194330 0x14000194330 <nil>} terms:[map[field:[data parent]] map[field:[data child]]] ts:1656367373130000 unique:true]
    System.out.println(
        client.query(
            CreateIndex(
                Obj(
                    "name", Value("unique_personLink_parent_child"),
                    "source", Collection(Value("PersonLink")),
                    "unique", Value(true),
                    "terms", Arr(
                        Obj("field", Arr(Value("data"), Value("parent"))),
                        Obj("field", Arr(Value("data"), Value("child")))
                    )
                )
            )
        ).get());
    {ref: ref(id = "unique_personLink_parent_child", collection = ref(id = "indexes")), ts: 1651094261520000, active: true, serialized: true, unique: true, name: "unique_personLink_parent_child", source: ref(id = "PersonLink", collection = ref(id = "collections")), terms: [{field: ["data", "parent"]}, {field: ["data", "child"]}], partitions: 1}
    client.query(
      q.CreateIndex(
        {
          name: 'unique_personLink_parent_child',
          source: q.Collection('PersonLink'),
          terms: [
            { field: ['data', 'parent'] },
            { field: ['data', 'child'] },
          ],
          unique: true,
        },
      )
    )
      .then((ret) => console.log(ret))
      .catch((err) => console.error(
        'Error: [%s] %s: %s',
        err.name,
        err.message,
        err.errors()[0].description,
      ))
    {
      ref: Index("unique_personLink_parent_child"),
      ts: 1651094261520000,
      active: true,
      serialized: true,
      name: "unique_personLink_parent_child",
      unique: true,
      source: Collection("PersonLink"),
      terms: [
        {
          field: ["data", "parent"]
        },
        {
          field: ["data", "child"]
        }
      ],
      partitions: 1
    }
    result = client.query(
        q.create_index(
            {
                "name": "unique_personLink_parent_child",
                "source": q.collection("PersonLink"),
                "unique": True,
                "terms": [
                    {"field": ["data", "parent"]},
                    {"field": ["data", "child"]}
                ]
            }
        )
    )
    print(result)
    {'ref': Ref(id=unique_personLink_parent_child, collection=Ref(id=indexes)), 'ts': 1656004338420000, 'active': True, 'serialized': True, 'name': 'unique_personLink_parent_child', 'source': Ref(id=PersonLink, collection=Ref(id=collections)), 'unique': True, 'terms': [{'field': ['data', 'parent']}, {'field': ['data', 'child']}], 'partitions': 1}
    CreateIndex({
      name: "unique_personLink_parent_child",
      source: Collection("PersonLink"),
      unique: true,
      terms: [
        { field: ["data", "parent" ] },
        { field: ["data", "child" ] }
      ]
    })
    {
      ref: Index("unique_personLink_parent_child"),
      ts: 1651094261520000,
      active: true,
      serialized: true,
      name: 'unique_personLink_parent_child',
      unique: true,
      source: Collection("PersonLink"),
      terms: [ { field: [ 'data', 'parent' ] }, { field: [ 'data', 'child' ] } ],
      partitions: 1
    }
    Query metrics:
    •    bytesIn:   203

    •   bytesOut:   371

    • computeOps:     1

    •    readOps:     0

    •   writeOps:     7

    •  readBytes: 2,449

    • writeBytes: 1,481

    •  queryTime:  12ms

    •    retries:     0

  7. Query all children and parent relations of a person

    Now that the parent/child relations are in place, let’s take a look to verify.

    For the next query, copy the _id value for the Person1 document (in the response for step 5) and replace the <$PERSON_ID> string with that value.

    query FindParentsAndChildren {
      findPersonByID(id:"<$PERSON_ID>") {
        name
        parents {
          data{
            parent {
              name
            }
          }
        }
        children {
          data{
            child {
              name
            }
          }
        }
      }
    }
    {
      "data": {
        "findPersonByID": {
          "name": "Person1",
          "parents": {
            "data": [
              {
                "parent": {
                  "name": "Parent1"
                }
              },
              {
                "parent": {
                  "name": "Parent2"
                }
              }
            ]
          },
          "children": {
            "data": [
              {
                "child": {
                  "name": "Child1"
                }
              },
              {
                "child": {
                  "name": "Child2"
                }
              },
              {
                "child": {
                  "name": "Child3"
                }
              }
            ]
          }
        }
      }
    }

Conclusion

This tutorial has demonstrated how to setup GraphQL many-to-many self-referential relations.

For more information on GraphQL relations, see Relations.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!