Relationships

Without relationships, your type definitions are simply 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 section will run through writing some type definitions for a simple connected model, inserting some data through the schema, and then querying it.

Example graph

The following graph will be used in this example, where a Person type has two different relationship types which can connect it to a Movie type.

relationships
Figure 1. Example graph

Type definitions

First, to define the nodes, you should define the two distinct types in this model:

type Person {
    name: String!
    born: Int!
}

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

You can then connect these two types together using @relationship directives:

type Person {
    name: String!
    born: Int!
    actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
    directedMovies: [Movie!]! @relationship(type: "DIRECTED", direction: OUT)
}

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

The following should be noted about the fields you just added:

  • A Person can act in or direct multiple movies, and a Movie can have multiple actors. However, it is exceedingly rare for a Movie to have more than one director, and you can model this cardinality in your type definitions, to ensure accuracy of your data.

  • A Movie isn’t really a Movie without a director, and this has been signified by marking the director field as non-nullable, meaning that a Movie must have a DIRECTED relationship coming into it.

  • To figure out whether the direction argument of the @relationship directive should be IN or OUT, visualise your relationships like in the diagram above, and model out the direction of the arrows.

  • The @relationship directive is a reference to Neo4j relationships, whereas in the schema, the phrase edge(s) is used to be consistent with the general API language used by Relay.

Relationship Properties

Relationship properties can be added to the above type definitions in two steps:

  1. Add an interface definition containing the desired relationship properties

  2. Add a properties argument to both "sides" of the @relationship directive which points to the newly defined interface

For example, to distinguish which roles an actor played in a movie:

type Person {
    name: String!
    born: Int!
    actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
    directedMovies: [Movie!]! @relationship(type: "DIRECTED", direction: OUT)
}

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

interface ActedIn @relationshipProperties {
    roles: [String!]
}

QueryDirection

All relationships have a direction, however, when querying it is possible to perform undirected queries. When defining a relationship you can define the default behaviour when performing queries over this relationship with the argument queryDirection:

type Person {
    name: String!
    born: Int!
    actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, queryDirection: DEFAULT_DIRECTED)
}

queryDirection can have the following values:

  • DEFAULT_DIRECTED (default): All queries will be directed by default, but users may perform undirected queries.

  • DEFAULT_UNDIRECTED: All queries will be undirected by default, but users may perform directed queries.

  • DIRECTED_ONLY: Only directed queries can be perform on this relationship.

  • UNDIRECTED_ONLY: Only undirected queries can be perform 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. You can’t create a Movie without adding a director, and you can do that by either creating the director first and then creating and connecting the movie, or you can create both the Movie and the director in the same mutation. With the latter approach:

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: "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.

As you can see, these nested mutations are very powerful, and in the second Mutation you ran, you were able to return the entire graph which was created in this example. In fact, these mutations can actually be compressed down into a single Mutation which 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
                    }
                }
            }
        }
    }
}

Once you get your head around this, you’ll be creating giant sub-graphs in one Mutation in no time!

Fetching your data

Now that you have the Movie information in your database, you can query all of the information which you just inserted as follows:

query {
    movies(where: { title: "Forrest Gump" }) {
        title
        released
        director {
            name
            born
        }
        actorsConnection {
            edges {
                roles
                node {
                    name
                    born
                }
            }
        }
    }
}

Cardinality

The Neo4j GraphQL Library has type definition requirements for "many" relationships, so given this example:

type User {
    name: String!
    posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT)
}

type Post {
    name: String!
}

The relationship at User.posts is considered a "many" relationship. Relationships such as the one above should always be of type NonNullListType and NonNullNamedType, meaning both the array and the type inside of it should have a !.