GraphQL relations
The Fauna GraphQL API recognizes relationships based on the fields in the GraphQL schema imported. 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 make sure to consistently read 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 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.
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
car
s, 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 Car
s, 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 Car
s 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 Car
s.
There is not enough evidence to categorize this relationship as
one-to-one since the same User
can be associated with multiple
Car
s.
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 Car
s, while a
Car
can be driven by many User
s.
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 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.
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
Car
s 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",
}
}
}
}
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 Car
s.
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!