Relationship Operations

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.

Relationships are a fundamental part of the Neo4j database, and the Neo4j GraphQL Library generates a comprehensive set of operations for working with them. These operations can be combined, enabling you to perform complex graph manipulations in a single mutation.

This page demonstrates the various relationship operations available and how to use them effectively in your GraphQL schema through a sequence of scenarios and equivalent mutations to update "the City database".

Data model

Consider the following data model graph:

2 relationships to itself
Figure 1. Model of a Person node connected to Person nodes through KNOWS and HAS_FRIEND relationships

Writing type definitions

Define the nodes and relationships as described in the relationships page and make sure to include the relationship properties as needed.

Defining the Person node with two relationship types, KNOWS and HAS_FRIEND
type Person @node {
    name: String!
    friends: [Person!]! @relationship(type: "HAS_FRIEND", direction: OUT, properties: "Friendship")
    acquaintances: [Person!]! @relationship(type: "KNOWS", direction: OUT)
}
type Friendship @relationshipProperties {
    since: Int
}

Queries

queryDirection

All relationships have a direction. However, when querying them, it is possible to perform undirected queries, that is, queries that ignore the relationship direction.

Use the argument queryDirection to control this behavior:

Updating the KNOWS relationship field with the queryDirection argument
type Person @node {
    name: String!
    friends: [Person!]! @relationship(type: "HAS_FRIEND", direction: OUT)
    acquaintances: [Person!]! @relationship(type: "KNOWS", direction: OUT, queryDirection: UNDIRECTED)
}

queryDirection can have the following values:

  • DIRECTED: only directed queries can be performed on this relationship. This is the default value.

  • UNDIRECTED: only undirected queries can be performed on this relationship.

Fetching your data

Consider the following sample data in the database:

Person to Person 2 relationships
Figure 2. Two Person nodes connected through KNOWS and HAS_FRIEND relationships

Using the updated type definition from queryDirection, notice the difference in the response when querying for both relationship fields: friends takes into account the direction of the HAS_FRIEND relationship while acquaintances ignores the direction of the KNOWS relationship and returns all related nodes.

Example 1. friends only applies from Alice to Bob, while acquaintances applies in both directions
query {
    people {
        name
        friends {
            name
        }
        acquaintances {
            name
        }
    }
}
{
    "data": {
        "people": [
            {
                "name": "Alice",
                "friends": [{ "name": "Bob" }],
                "acquaintances": [{ "name": "Bob" }]
            },
            {
                "name": "Bob",
                "friends": [],
                "acquaintances": [{ "name": "Alice" }]
            }
        ]
    }
}

For Alice to be returned in Bob’s friends list, another relationship of type HAS_FRIEND, directed from Bob to Alice, must exit in the database.

Example 2. Using Cypher to add the missing HAS_FRIEND relationship from Bob to Alice

Setup:

MATCH (alice:Person {name: "Alice"})
MATCH (bob:Person {name: "Bob"})
MERGE (bob)-[:HAS_FRIEND]->(alice)

Running the same query again now returns Alice in Bob’s friends list:

{
    "data": {
        "people": [
            {
                "name": "Alice",
                "friends": [{ "name": "Bob" }],
                "acquaintances": [{ "name": "Bob" }]
            },
            {
                "name": "Bob",
                "friends": [{ "name": "Alice" }],
                "acquaintances": [{ "name": "Alice" }]
            }
        ]
    }
}

In order to be traversable from both sides, directed relationships have to exist in each direction, while undirected relationships only have to exist in one direction.

Fetching your data using a connection query

The GraphQL library can generate connection fields for relationship fields, which allow you to query for relationship properties and other metadata about the relationship. These fields are compliant with the Relay-style connection specification.

The query in the following example requests the same data as above but using the connection fields for both relationships.

Example 3. Get Person nodes with related nodes through connection fields
query {
    peopleConnection {
        edges {
            node {
                name
                friendsConnection {
                    edges {
                        node {
                            name
                        }
                        # can now request relationship properties
                        properties {
                            since
                        }
                    }
                }
                acquaintancesConnection {
                    edges {
                        node {
                            name
                        }
                    }
                }
            }
        }
    }
}

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

Mutations

The top level operations refer to nodes in the database. For the type definitions in Writing type definitions, the generated top level operations are:

Generated mutation fields (top level operations)
createPeople(input: [PersonCreateInput!]!)!: CreatePeopleMutationResponse!
updatePeople(where: PersonWhere!, update: PersonUpdateInput!)!: UpdatePeopleMutationResponse!
deletePeople(where: PersonWhere!, delete: PersonDeleteInput!)!: DeleteInfo!

As arguments to the top level operations, the Neo4j GraphQL Library generates a set of nested operations which manage the relationships between the nodes in the database.

Terminology

  • The target of the top level operation becomes the "source node" of the relationship.

  • Through the nested operations, the created or traversed relationships have the top level matched node as the source node.

  • The nodes on the other side of the relationship are called the "target nodes".

  • The nested operations allow CRUD operations for the target nodes and for the relationships between the source node and the target nodes.

  • Creation and deletion of nodes use the nomenclature "create" and "delete", while creation and deletion of relationships use the nomenclature "connect" and "disconnect".

The nested operations are:

  • The create operation creates the target node, together with the relationship between the source and target nodes in the same operation.

  • The connect operation only creates the relationship between the source node and target nodes that already exist in the database.

  • The delete operation deletes the target node, together with all relationships connected to it.

  • The disconnect operation only deletes the relationship between the source node and the target node, without deleting either of the nodes.

  • The update operation updates the properties of the target node, as well as the relationship between the source and target nodes.

Each of the top level operations support their own set of nested operations:

Generated input types of top level operations, focusing on nested operations for relationship HAS_FRIEND
# Top level create operation supports nested:
# * create
# * connect
input PersonCreateInput {
    # ...other fields
    friends: PersonFriendsFieldInput
}
input PersonFriendsFieldInput {
    connect : [PersonFriendsConnectFieldInput!]
    create : [PersonFriendsCreateFieldInput!]
}

# Top level update operation supports nested:
# * create
# * connect
# * update
# * disconnect
# * delete
input PersonUpdateInput {
    # ...other fields
    friends: [PersonFriendsUpdateFieldInput!]
}
input PersonFriendsUpdateFieldInput {
    connect : [PersonFriendsConnectFieldInput!]
    create : [PersonFriendsCreateFieldInput!]
    delete : [PersonFriendsDeleteFieldInput!]
    disconnect : [PersonFriendsDisconnectFieldInput!]
    update : PersonFriendsUpdateConnectionInput
}

# Top level delete operation supports nested:
# * delete
input PersonDeleteInput {
    # ...other fields
    friends: [PersonFriendsDeleteFieldInput!]
}
input PersonFriendsDeleteFieldInput {
    delete: PersonDeleteInput
}

Create and connect

Consider the following scenario: Bob goes to a party where he meets Charlie and Dave. When asked, Charlie says he considers Bob to be a friend, but Dave spoke too little so he is just an acquaintance for now.

Charlie is not in the database so the Person node needs to be created. This indicates that the create top level operation can be used.

Note that when querying, the acquaintances relationship is undirected, so when Charlie says he became acquainted with Dave, the reverse is also true without needing to run another query.

Example 4. Top level create with nested connect and create operations
mutation {
    createPeople(input: [{
        name: "Charlie",
        # Charlie is the source node
        # creates the relationship HAS_FRIEND
        friends: { connect: { where: { node: { name: { eq: "Bob" } } } } },
        # creates the node Dave and the relationship KNOWS
        acquaintances: { create: { node: { name: "Dave" } } }
    }]) {
        people {
            name
            friends {
                name
            }
            acquaintances {
                name
            }
        }
    }
}

Consider the following: Bob shares Charlie’s sentiment and also considers him a friend while Dave is an acquaintance.

Bob’s node exists in the database so his node has to be updated to reflect these new relationships. This indicates a use case for the top-level update operation.

Example 5. Top level update with nested connect and create operations
mutation {
    updatePeople(
        where: { name: "Bob" },
        update: {
            # Bob is the source node
            friends: {
                connect: { where: { node: { name: { eq: "Charlie" } } } },
                create: { node: { name: "Camilla" } }
            },
            acquaintances: { connect: { where: { node: { name: { eq: "Dave" } } } } }
        }
    ) {
        people {
            name
            friends {
                name
            }
            acquaintances {
                name
            }
        }
    }
}

Update

Consider the following scenario: Bob and Camilla are getting married and changing their last names to B.

Both nodes already exist in the database, indicating a use case for the top level update operation.

Since their nodes are related through the friends relationship, you can perform both changes in the same mutation starting with a top level update operation.

Caveat: the database only contains one friends relationship, directed from Bob to Camilla. This means that the top level update operation has to be performed on Bob’s node, as Camilla’s node does not meet the filter criteria for the nested update operation.

Read along the queryDirection and mutations section to understand how the queryDirection argument of the @relationship directive affects this caveat.

Example 6. Top level update with nested update operation
mutation {
    updatePeople(
        where: { name: { eq: "Bob" } },
        update: {
            # update Bob node's fields
            name: { set: "Bob B." },
            # Bob is the source node
            friends: [{
                update: {
                    # Camilla is the target node
                    where: {
                        node: { name: { eq: "Camilla" } },
                    },
                    # update Camilla node's fields
                    node: { name: { set: "Camilla B." } },
                    # update the relationship between Bob and Camilla
                    edge: { since: { set: 2022 } },
                },
            }],
        }
    ) {
    info {
        nodesCreated
        relationshipsCreated
    }
  }
}

Delete and disconnect

Consider the following scenario: Alice notified everyone at the party that she moving away. Bob did not like how Charlie behaved at hearing the news and wants to cut ties with him.

Alice’s node must be deleted from the database, indicating a use case for the top level delete operation.

However, both actions of the scenario have Bob in common, so a valid alternative is a top level update operation on Bob’s node with nested operations to perform the correct changes on Alice and Charlie nodes.

Note that this is only possible because Bob has relationships to both. There must be a relationship of type HAS_FRIEND from Bob to Alice in order for the node Alice to meet the filter criteria for the nested delete operation, and the same for Charlie and the nested disconnect operation.

Example 7. Top level update with nested disconnect and delete operations
mutation {
    updatePeople(
        where: { name:  { eq: "Bob B." } },
        update: {
            # Bob B. is the source node
            friends: [
                {
                    # deletes Alice and all relationships between Alice and all other nodes (including the KNOWS relationships)
                    delete: [ { where: { node: { name: { eq: "Alice" } } } } ],
                },
                {
                    # deletes the relationship between Bob B. and Charlie, without deleting either node
                    disconnect: [ { where: { node: { name: { eq: "Charlie" } } } } ],
                },
            ],
        }
    ) {
    info {
      nodesDeleted
      relationshipsDeleted
    }
  }
}

Consider also: Bob and Camilla are moving away together.

Both nodes need to be deleted, together with all relationships connected to them, indicating a use case for the top level delete operation.

Since the nodes are related through the friends relationship, both deletions can be performed in the same mutation starting with a delete operation.

Similar to the Update section, there is a caveat: the database only contains one friends relationship, from Bob to Camilla. This means that the top level delete operation must be performed on Bob’s node, as Camilla’s node does not meet the filter criteria for the nested delete operation.

Here’s the outcome of running the same mutation in both directions:

Example 8. Top level delete with nested delete operation matching 0 relationships
mutation {
    deletePeople(
        # deletes Camilla and all relationships between Camilla and all other nodes
        where: { name: { eq: "Camilla B." } },
        delete: {
            # Camilla is the source node
            friends: [
                {
                    # no relationship of type HAS_FRIEND matching this criteria exists
                    where: { node: { name: { eq: "Bob B." } } }
                },
            ],
        }
    ) {
    nodesDeleted
    relationshipsDeleted
  }
}

This results in the deletion of Camilla node and the relationship between Bob and Camilla:

{
  "data": {
    "deletePeople": {
      "nodesDeleted": 1,
      "relationshipsDeleted": 1
    }
  }
}
Example 9. Top level delete with nested delete operation matching the correct node
mutation {
    deletePeople(
        # deletes Bob and all relationships between Bob and all other nodes
        where: { name: { eq: "Bob B." } },
        delete: {
            # Bob is the source node
            friends: [
                {
                    # deletes Camilla and all relationships between Camilla and all other nodes (including the KNOWS relationships)
                    where: { node: { name: { eq: "Camilla B." } } }
                },
            ],
        }
    ) {
    nodesDeleted
    relationshipsDeleted
  }
}

This results in the deletion of both Bob’s and Camilla’s nodes and the relationship between them:

{
  "data": {
    "deletePeople": {
      "nodesDeleted": 2,
      "relationshipsDeleted": 1
    }
  }
}

Read along the queryDirection and mutations section to understand how the queryDirection argument of the @relationship directive affects the outcome of these mutations.

queryDirection and mutations

The queryDirection argument of the @relationship directive affects database queries.

In terms of GraphQL operations this means that both queries and mutations are affected by its value.

When performing mutations, the nodes and relationships specified in the mutation input must be queried from the database first.

That is when the queryDirection argument is relevant, as it determines whether the relationships between the matched nodes and their related nodes are taken into account in a directed or undirected fashion for the execution of the mutation.

For a similar mutation to Top level delete with nested delete operation matching the correct node but with an acquaintances instead of friends relationship from Bob to Camilla, the outcome changes as follows:

Example 10. Top level delete with nested delete operation on an undirected relationship

Setup:

CREATE (bob:Person {name: "Bob B."})
CREATE (camilla:Person {name: "Camilla B."})
MERGE (bob)-[:KNOWS]->(camilla)

Mutation:

mutation {
    deletePeople(
        # deletes Camilla and all relationships between Camilla and all other nodes
        where: { name: { eq: "Camilla B." } },
        delete: {
            # Camilla is the source node
            acquaintances: [
                {
                    # because the relationship is undirected, this filter matches the relationship between Bob and Camilla, even if it is directed from Bob to Camilla in the database
                    where: { node: { name: { eq: "Bob B." } } }
                },
            ],
        }
    ) {
    nodesDeleted
    relationshipsDeleted
  }
}

This results the deletion of both Bob’s and Camilla’s node and the relationship between them:

{
  "data": {
    "deletePeople": {
      "nodesDeleted": 2,
      "relationshipsDeleted": 1
    }
  }
}