Relationships

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.

relationships
Figure 1. Example graph

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 {
    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)
}

Note that, in this query:

  • A Person can act in or direct multiple movies, and a Movie can have multiple actors. However, it is rare for a Movie 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 a Movie without a director, and this has been signified by marking the director field as non-nullable. This means that a Movie must have a DIRECTED relationship coming into it to be valid.

  • To figure out whether the direction argument of the @relationship directive should be IN or OUT, 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 phrase edge(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:

  1. Add a type definition decorated with the @relationshipProperties directive, containing the desired relationship properties.

  2. 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 {
    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)
}

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

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 {
    name: String!
    born: Int!
    actedInMovies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
}

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

type Series implements Production {
    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 {
    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 are directed by default, but users may perform undirected queries.

  • DEFAULT_UNDIRECTED: all queries are undirected by default, but users may perform directed queries.

  • DIRECTED_ONLY: only directed queries can be performed on this relationship.

  • UNDIRECTED_ONLY: 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: "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: "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 {
    name: String!
    posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT)
}

type Post {
    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 !.