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.
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 aDIRECTED
relationship coming into it. -
To figure out whether the
direction
argument of the@relationship
directive should beIN
orOUT
, 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:
-
Add an interface definition containing the desired relationship properties
-
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 !
.
Was this page helpful?