Cardinality and modeling relationships

This is the documentation of the GraphQL Library version 7. For the long-term support (LTS) version 5, refer to GraphQL Library version 5 LTS.

Irrespective of the data model you have in your Neo4j database, the GraphQL schema should be designed in a way that allows the access of the data that your application needs. This means that there are often multiple ways to model the GraphQL schema given the same type of data in the database, each of which has benefits and tradeoffs.

This page describes the different ways of modeling a Neo4j relationship in a GraphQL schema.

Data model

Take the following graph as an example in which a Person type has two different relationship types, which can connect it to a Movie type.

2 relationship types
Figure 1. Model of a Movie node connected to a Person node through two different relationship types: ACTED_IN and DIRECTED.

Modeling relationships

The data model represents represents the state of the graph in the database, but there are multiple ways to model the relationships in the GraphQL schema.

The way you model the relationships in the GraphQL schema affects how you can query and manipulate the data, and it is important to choose a model that fits the needs of your application.

The type definitions below do not contain any relationship fields but are equally valid as a GraphQL schema, with the only difference being that they do not allow for traversing the relationships between Person and Movie nodes.

Defining the node types without relationship fields
type Person @node {
    name: String!
}

type Movie @node {
    title: String!
    released: Int!
}

Similarly, a relationship does not have to be modeled on both sides of the relationship. While the underlying relationship in the database must have a source and a target node, it is possible for the GraphQL schema to only describe one side of the relationship.

Modeling a relationship only on one side of the relationship

In the following type definitions, a Movie has access to the Person nodes through the actors relationship field, but a Person does not have access to the Movie nodes.

type Person @node {
    name: String!
}

type Movie @node {
    title: String!
    released: Int!
    actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
}

Many-to-many relationships

A Person can act in multiple movies, and a Movie can have multiple actors acted in it.

Conceptually this is a many-to-many relationship, which is modeled by using list types for both relationship fields on both sides of the relationship.

Adding relationship fields both ways for a many-to-many relationship of type ACTED_IN between Person and Movie
type Person @node {
    name: String!
    actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}

type Movie @node {
    title: String!
    released: Int!
    actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
}

Both the list type and the type inside the list must be non-nullable, meaning that the relationship field must always return an array of values, and that array cannot contain null values. This means there is always a number of related nodes (even if that number is zero), and the related nodes are always valid nodes (and never null).

In other words, both the array and the type inside must have a !.

One-to-many relationships

A Person can direct multiple movies but a Movie only has one director.

Conceptually this is a one-to-many relationship, which is modeled by using a list type for the relationship field on the Person type, and a non-list type for the relationship field on the Movie type.

Adding relationship fields both ways for a one-to-many relationship of type DIRECTED between Person and Movie
type Person @node {
    name: String!
    actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
    directed: [Movie!]! @relationship(type: "DIRECTED", direction: OUT)
}

type Movie @node {
    title: String!
    released: Int!
    actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
    director: Person @relationship(type: "DIRECTED", direction: IN)
}

On the "many" side of the relationship, the list type and the type inside the list must be non-nullable (see many-to-many relationships). This means that the relationship field must always return an array of values, and that array cannot contain null values.

On the "one-to" side of the relationship, the type must be nullable, meaning that the relationship field can return null if there is no related node.

One-to-one relationships

A Person Ana can marry only one other Person Bob, and that other Person Bob can only be married to that Person Ana.

Conceptually this is a one-to-one relationship, which is modeled by using non-list types for the relationship field on both sides of the relationship.

Adding relationship fields both ways for a one-to-one relationship of type MARRIED between Person and Person
type Person @node {
    name: String!
    spouse: Person @relationship(type: "MARRIED", direction: OUT)
}

In this case, the relationship is between nodes of the same type. This does not need to be the case and is only for the sake of this example.

The relationship field type must be nullable, meaning that the relationship field can return null if there is no related node.

Cardinality

A non-list relationship field on a node type (the "one" side of a one-to-one or one-to-many relationship) is called a single relationship field.

Neo4j databases do not currently support constraints on relationship cardinality and therefore users must be mindful of the potential for data integrity issues in the case of APIs that allow for data manipulation on single relationships.