Relationships
This is the documentation of the GraphQL Library version 6. For the long-term support (LTS) version 5, refer to GraphQL Library version 5 LTS. |
Without relationships, your type definitions work rather as a collection of disconnected nodes, with little value. Adding relationships into your data model gives your data the context that it needs to run complex queries across wide sections of your graph.
This page describes how to write type definitions for a simple connected model, inserting data through the schema, and then querying it.
Type definitions
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.
To create that graph using the Neo4j GraphQL Library, first you need to define the nodes and define the two distinct types in this model:
type Person @node {
name: String!
born: Int!
}
type Movie @node {
title: String!
released: Int!
}
You can then connect these two types together using @relationship
directives:
type Person @node {
name: String!
born: Int!
actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
directedMovies: [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)
}
Note that, in this query:
-
A
Person
can act in or direct multiple movies, and aMovie
can have multiple actors. However, it is rare for aMovie
to have more than one director, so you can model this cardinality in your type definitions, to ensure accuracy of your data. -
A
Movie
isn’t really aMovie
without a director, and this has been signified by marking thedirector
field as non-nullable. This means that aMovie
must have aDIRECTED
relationship coming into it to be valid. -
To figure out whether the
direction
argument of the@relationship
directive should beIN
orOUT
, visualize your relationships like in the diagram above, then model out the direction of the arrows. -
The
@relationship
directive is a reference to Neo4j relationships, whereas in the schema, the phraseedge(s)
is used to be consistent with the general API language used by Relay.
Relationship properties
You can add relationship properties to the example in two steps:
-
Add a type definition decorated with the
@relationshipProperties
directive, containing the desired relationship properties. -
Add a
properties
argument to both "sides" (or just one side, if you prefer) of the@relationship
directive which points to the newly defined interface.
For example, suppose you want to distinguish which roles an actor played in a movie:
type Person @node {
name: String!
born: Int!
actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
directedMovies: [Movie!]! @relationship(type: "DIRECTED", direction: OUT)
}
type Movie @node {
title: String!
released: Int!
actors: [Person!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN)
director: Person! @relationship(type: "DIRECTED", direction: IN)
}
type ActedIn @relationshipProperties {
roles: [String!]
}
@declareRelationship
If you need a relationship to be available on interfaces, you need to use the new @declareRelationship
directive instead, as well as define the relationships in the concrete type:
interface Production {
title: String!
actors: [Actor!]! @declareRelationship
}
type Actor @node {
name: String!
born: Int!
actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
}
type Movie implements Production @node {
title: String!
released: Int!
actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN)
}
type Series implements Production @node {
title: String!
released: Int!
episodes: Int!
actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN)
}
type ActedIn @relationshipProperties {
roles: [String!]
}
queryDirection
All relationships have a direction.
However, when querying them, it is possible to perform undirected queries.
To set the default behavior of a relationship when it is queried, you can use the argument queryDirection
:
type Person @node {
name: String!
born: Int!
actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, queryDirection: DEFAULT_DIRECTED)
}
queryDirection
can have the following values:
-
DIRECTED
: only directed queries can be performed on this relationship. -
UNDIRECTED
: only undirected queries can be performed on this relationship.
Inserting data
Nested mutations mean that there are many ways in which you can insert data into your database through the GraphQL schema.
Consider the previously mentioned rule that a Movie
node cannot be created without adding a director.
You can, however, create a director node first and then create and connect it to a Movie
.
Another option is to create both Movie
and Director
in the same mutation, for example:
mutation CreateMovieAndDirector {
createMovies(input: [
{
title: "Forrest Gump"
released: 1994
director: {
create: {
node: {
name: "Robert Zemeckis"
born: 1951
}
}
}
}
]) {
movies {
title
released
director {
name
born
}
}
}
}
You then need to create the actor in this example, and connect them to the new Movie
node, also specifying which roles they played:
mutation CreateActor {
createPeople(input: [
{
name: "Tom Hanks"
born: 1956
actedInMovies: {
connect: {
where: {
node: { title_EQ: "Forrest Gump" }
}
edge: {
roles: ["Forrest"]
}
}
}
}
]) {
movies {
title
released
director {
name
born
}
actorsConnection {
edges {
roles
node {
name
born
}
}
}
}
}
}
Note the selection of the actorsConnection
field in order to query the roles
relationship property.
Also observe that, in the second mutation, the entire graph was returned. That is not necessary, since you can compress down these mutations into one single operation that inserts all of the data needed:
mutation CreateMovieDirectorAndActor {
createMovies(input: [
{
title: "Forrest Gump"
released: 1994
director: {
create: {
node: {
name: "Robert Zemeckis"
born: 1951
}
}
}
actors: {
create: [
{
node: {
name: "Tom Hanks"
born: 1956
}
edge: {
roles: ["Forrest"]
}
}
]
}
}
]) {
movies {
title
released
director {
name
born
}
actorsConnection {
edges {
roles
node {
name
born
}
}
}
}
}
}
Acknowledging this helps you create bigger sub-graphs in one mutation at once and, therefore, more efficiently.
Fetching your data
Now that you have the Movie
information in your database, you can query everything altogether as follows:
query {
movies(where: { title_EQ: "Forrest Gump" }) {
title
released
director {
name
born
}
actorsConnection {
edges {
roles
node {
name
born
}
}
}
}
}
Cardinality
The Neo4j GraphQL Library has type definition requirements for "many" relationship. For example:
type User @node {
name: String!
posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT)
}
type Post @node {
name: String!
}
The relationship at User.posts
is considered a "many" relationship, which means it should always be of type NonNullListType
and NonNullNamedType
.
In other words, both the array and the type inside of a "many" relationship should have a !
.