GraphQL relationships

The Fauna GraphQL API recognizes relationships based on the fields in the imported GraphQL schema. The relationship recognition algorithm looks at every field for all non-embedded types. If it finds a field in the source type where its return is also non-embedded type, it tries to find a field in the target type that points back to the source. The resulting relationship cardinality depends on the presence, absence, or cardinality of matching fields found.

During schema import, the GraphQL API creates the necessary collections and indexes to correlate the data based on the recognized relationships in the schema.

It is important to notice that the cardinality of the relationship depends on the direction observed. A one-to-many relationship from left to right is interpreted as a many-to-one relationship when evaluated from right to left. The GraphQL API reads consistently from the shared database components that are required to fulfill relational queries regardless of which side of the relationship is queried.

If, during import, the relationship recognition algorithm fails to recognize a relationship, it returns an error requiring that relationships must be made explicit. Relationships can be made explicit by adding the @relation directive, with the same relation name, at both fields involved in the relationship.

Relationship names must be unique per type, which means that there cannot be multiple relationships for a single field in a type. Multiple relationships for a type each have unique names and so are not constrained.

This section describes the One-to-one, One-to-many, Many-to-one, and Many-to-many relationships, as well as how to combine multiple relationships, and relational mutations.

One-to-one

A one-to-one relationship puts a low cardinality constraint in a relationship between two types. It is represented in a GraphQL schema by two types where each one of them has a field that points to each other.

In the following example, a User can own a Car, while a Car can be owned by a single User. If we try to associate a User to different cars, we get a unique constraint violation error.

type User {
  name: String!
  car: Car
}

type Car {
  plate: String!
  owner: User
}

In the database, a single collection is predictably chosen to store relational data. If the Car collection is selected, its owner field contains a reference the associated User document while there is no car field in the User documents. A unique index is created on the Car collection with the owner field as a single term to enforce the low cardinality constraint.

One-to-many

A one-to-many relationship has high cardinality at only one side of the relationship. It is represented in the GraphQL schema by two types where the source has an array field that points to the target type, while the target type has a non-array field that points back to the source type.

In the following example, a User can have many Cars, while a Car can be associated with a single User.

type User {
  name: String!
  cars: [Car!] @relation
}

type Car {
  plate: String!
  owner: User!
}

Please note that we added the @relation directive to the cars field, otherwise the field would be stored an array of IDs instead of establishing a high cardinality relationship.

In the database, the collection with the non-array field is used to store relational data. In this case, the owner field in the Car documents contain a reference to the associated User document. A non-unique index is created on the Car collection with the owner field as a single term to allow the GraphQL API to read all Cars associated with a given User. There is no cars field in the User documents.

Many-to-one

The many-to-one relationship works the same as the one-to-many relationship. They are technically the same relationship, but a many-to-one relationship is evaluated from the other side. However, there is one particular derivation difference: the array field can be omitted.

In the following example, a Car can be associated with a single User, while a User is unaware of its association with Cars. There is not enough evidence to categorize this relationship as one-to-one since the same User can be associated with multiple Cars.

type User {
  name: String!
}

type Car {
  plate: String!
  owner: User!
}

The underlying database components and API behavior are identical to a one-to-many relationship.

Many-to-many

A many-to-many relationship has high cardinality on both sides of the relationship. It is represented in the GraphQL schema by two types, each with fields that point to each other, where all fields are arrays.

In the following example, a User can drive many Cars, while a Car can be driven by many Users.

type User {
  name: String!
  drives: [Car!] @relation
}

type Car {
  plate: String!
  drivers: [User!] @relation
}

Note that the @relation directive was added to both the drivers and drives fields; otherwise they would be stored arrays of IDs instead of establishing a high-cardinality relationship.

In the database, neither relational fields exist in the User or Car documents. Instead, an associative collection (also known as a jump table) is created to store references to each side of the relationship. Three indexes are created on the associative collection, one with the User reference as a term, the other with the Car reference as a term, and one with both the User and Car references as terms. The created indexes are used by the GraphQL API to read the associated documents, depending on which side of the relationship is queried.

Many-to-many relationship diagram

Combining multiple relationships

When describing complex types in the GraphQL schema, the API may not be able to precisely recognize relationships. For example:

type User {
  name: String!
  owns: Car!
}

type Car {
  plate: String!
  owner: User!
  driver: User!
}

In the schema above, there is the owns field in the User type that refers to a Car. However, there are multiple fields in the Car type that refer back to the User type. The API is not able to decide which field should be used to form the relationship, so it returns an error asking that the relationship be made explicit.

To make a relationship explicit, add the @relation directive with the same name to both fields that participate in the relationship. For example:

type User {
  name: String!
  owns: Car! @relation(name: "car_owner")
}

type Car {
  plate: String!
  owner: User! @relation(name: "car_owner")
  driver: User!
}

Now the API is able to recognize owns and owner as being part of a one-to-one relationship between User and Car. The driver field in the Car type is recognized as a many-to-one relationship.

When dealing with different relationships between the same types, it’s a good practice to make them explicit by using the @relation directive.

Relational mutations

When running mutations on types that contain relationships, you can create their related documents, or influence the relationship to existing documents, in the same transaction by using the relational mutations. There are three types of relational mutations: create, connect, and disconnect.

create

The create mutation allows the creation of documents that are associated with the main target of the mutation. In the following example, we create a new User and, in the same transaction, two Cars that are associated with the User.

Schema:

type User {
  name: String!
  cars: [Car!] @relation
}

type Car {
  plate: String!
  owner: User!
}

Query:

mutation {
  createUser(data: {
    name: "Jane"
    cars: {
      create: [
        { plate: "AAA-1234" }
        { plate: "BBB-123" }
      ]
    }
  }) {
    _id
    name
    cars {
      data {
        plate
      }
    }
  }
}

Response:

{
  "data": {
    "createUser": {
      "_id": "235184188140028419",
      "name": "Jane",
      "cars": {
        "data": [
          { "plate": "AAA-1234" },
          { "plate": "BBB-123" }
        ]
      }
    }
  }
}

connect

The connect mutation allows the connection of the target document to an existing document. In the following example, we create a Car and connect it to an existing User.

Query:

mutation {
  createCar(data: {
    plate: "CCC-123"
    owner: {
      connect: "235184188140028419"
    }
  }) {
    _id
    plate
    owner {
      name
    }
  }
}

Response:

{
  "data": {
    "createCar": {
      "_id": "235184601841009156",
      "plate": "CCC-123",
      "owner": {
        "name": "Jane",
      }
    }
  }
}

If you need to connect multiple documents at once, for example, when a user sells one or more cars to another user, use an array. The following query updates an existing user and makes them the owner of several vehicles (in addition to the one they already own):

Query:

mutation {
  updateUser(id: "326418809708609570", data: {
    name: "Alfred"
    cars: {
      connect: [
        "326418777067487266",
        "326418777066438690"
      ]
    }
  }) {
    _id
    name
    cars {
      data {
        plate
      }
    }
  }
}

Response:

{
  "data": {
    "updateUser": {
      "_id": "326418809708609570",
      "name": "Alfred",
      "cars": {
        "data": [
          {
            "plate": "AAA-1234"
          },
          {
            "plate": "BBB-123"
          },
          {
            "plate": "CCC-1234"
          }
        ]
      }
    }
  }
}

disconnect

The disconnect mutation allows the disconnection of the target document from a connected document. In the following example, we update a User and disconnect it from one of its Cars.

Query:

mutation {
  updateUser(id: "235184188140028419", data: {
    name: "Jane"
    cars: {
      disconnect: "235184411358790151"
    }
  }) {
    _id
    cars {
      data {
        plate
      }
    }
  }
}

Response:

{
  "data": {
    "updateUser": {
      "_id": "235184188140028419",
      "cars": {
        "data": [
          { "plate": "AAA-1234" },
          { "plate": "BBB-123" }
        ]
      }
    }
  }
}

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!