关系操作

这是 GraphQL Library 7 版本的文档。对于长期支持 (LTS) 版本 5,请参考 GraphQL Library 5 LTS 版本

关系是 Neo4j 数据库的重要组成部分,Neo4j GraphQL 库为此生成了一套全面的操作方法。这些操作可以组合使用,使您能够在一次变更(mutation)中执行复杂的图操作。

本页面通过一系列场景和相应的变更操作,演示了可用的各种关系操作以及如何在 GraphQL 模式中有效使用它们来更新“城市数据库”。

数据模型

考虑以下数据模型图

2 relationships to itself
图 1. 一个 Person 节点通过 KNOWS 和 HAS_FRIEND 关系连接到其他 Person 节点的模型。

编写类型定义

按照关系页面中的描述定义节点和关系,如果需要,请确保包含关系属性。

定义 Person 节点,其包含指向同一 Person 类型的两种关系类型:KNOWS 和 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
}

查询

queryDirection

所有关系都有方向。但是,在查询它们时,可以执行无向查询,即忽略关系方向值的查询。

要设置关系的此种查询行为,可以使用 queryDirection 参数

使用 queryDirection 参数更新 KNOWS 关系字段
type Person @node {
    name: String!
    friends: [Person!]! @relationship(type: "HAS_FRIEND", direction: OUT)
    acquaintances: [Person!]! @relationship(type: "KNOWS", direction: OUT, queryDirection: UNDIRECTED)
}

queryDirection 可以具有以下值

  • DIRECTED:只能对此关系执行有向查询。这是默认值。

  • UNDIRECTED:只能对此关系执行无向查询。

获取数据

考虑数据库中的以下示例数据

Person to Person 2 relationships
图 2. 两个 Person 节点通过 KNOWS 和 HAS_FRIEND 关系连接。两种关系都有方向。

如上图所示,关系在数据库中必须具有方向。但这并不意味着在查询关系时必须考虑方向,因为这由 @relationship 指令的 queryDirection 参数决定。

请注意查询两个关系字段时响应的差异:friends 考虑了 HAS_FRIEND 关系的方向,而 acquaintances 忽略了 KNOWS 关系的方向,并返回所有相关节点,而不管数据库中该关系的方向如何。

示例 1. 获取带有相关节点的 Person 节点:friends 仅从 Alice 到 Bob 应用,而 acquaintances 从 Alice 到 Bob 和从 Bob 到 Alice 均适用
query {
    people {
        name
        friends {
            name
        }
        acquaintances {
            name
        }
    }
}
{
    "data": {
        "people": [
            {
                "name": "Alice",
                "friends": [{ "name": "Bob" }],
                "acquaintances": [{ "name": "Bob" }]
            },
            {
                "name": "Bob",
                "friends": [],
                "acquaintances": [{ "name": "Alice" }]
            }
        ]
    }
}

为了使 Alice 出现在 Bob 的 friends 列表中,数据库中必须存在另一个从 Bob 指向 Alice 的 HAS_FRIEND 类型关系。

示例 2. 有向关系必须在每个方向上都存在,才能从两侧遍历该关系;而无向关系只需存在于任意一个方向即可

设置

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

再次运行相同的查询,现在 Alice 出现在了 Bob 的 friends 列表中

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

使用连接查询获取数据

GraphQL 库可以为关系字段生成连接(connection)字段,允许您查询关系属性以及关于该关系的其他元数据。这些字段符合 Relay 风格的连接规范。

以下示例中的查询请求与上述相同的数据,但对两个关系都使用了连接字段。

示例 3. 通过连接字段获取带有相关节点的 Person 节点
query {
    peopleConnection {
        edges {
            node {
                name
                friendsConnection {
                    edges {
                        node {
                            name
                        }
                        # can now request relationship properties
                        properties {
                            since
                        }
                    }
                }
                acquaintancesConnection {
                    edges {
                        node {
                            name
                        }
                    }
                }
            }
        }
    }
}

@relationship 指令是对 Neo4j 关系的引用,而在编写查询时,使用 edge(s) 一词是为了与 Relay 使用的通用 API 语言保持一致。

变更(Mutations)

顶级操作始终引用数据库内的节点。对于上述仅包含 Person 类型的类型定义,生成的顶级操作将为:

生成的变更字段(顶级操作)
createPeople(input: [PersonCreateInput!]!)!: CreatePeopleMutationResponse!
updatePeople(where: PersonWhere!, update: PersonUpdateInput!)!: UpdatePeopleMutationResponse!
deletePeople(where: PersonWhere!, delete: PersonDeleteInput!)!: DeleteInfo!

作为顶级操作的参数,Neo4j GraphQL 库生成了一组嵌套操作,用于管理数据库中节点之间的关系。

术语

  • 顶级操作的目标成为关系的“源节点”。

  • 通过嵌套操作,创建/遍历的关系将顶级匹配的节点作为源节点。

  • 关系另一侧的节点称为“目标节点”。

  • 嵌套操作允许对目标节点以及源节点与目标节点之间的关系进行 CRUD 操作。

  • 节点的创建和删除使用术语“create”和“delete”,而关系的创建和删除使用术语“connect”和“disconnect”。

通过嵌套操作,您可以创建、更新或删除目标节点,以及源节点和目标节点之间的关系

  • create 操作创建目标节点,并同时在同一次操作中创建源节点和目标节点之间的关系。

  • connect 操作仅在数据库中已存在的源节点和目标节点之间创建关系。

  • delete 操作删除目标节点,以及与之连接的所有关系。

  • disconnect 操作仅删除源节点和目标节点之间的关系,而不删除任何节点。

  • update 操作更新目标节点的属性,以及源节点和目标节点之间的关系。

每个顶级操作都支持其自己的一组嵌套操作

顶级操作生成的输入类型,专注于关系 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
}

本节通过一系列场景和更新城市数据库的等效变更操作,进一步说明了不同的操作及其组合方式。

创建与连接

场景: Bob 去参加聚会,遇到了 Charlie 和 Dave。当被问及时,Charlie 说他认为 Bob 是朋友,但 Dave 说话太少,所以目前只是熟人。

Charlie 不在数据库中,因此需要创建 Person 节点。这意味着可以使用 create 顶级操作。

请注意,在查询时,acquaintances 关系是无向的,因此当 Charlie 说他与 Dave 结识时,反之亦然,无需运行其他查询。

示例 4. 带有嵌套 connect 和 create 操作的顶级 create
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
            }
        }
    }
}

场景: Bob 同意 Charlie 的看法,也认为他是朋友,而 Dave 是熟人。

Bob 的节点已存在于数据库中,因此必须更新他的节点以反映这些新关系。这表明了顶级 update 操作的用例。

示例 5. 带有嵌套 connect 和 create 操作的顶级 update
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
            }
        }
    }
}

更新

场景: Bob 和 Camilla 要结婚了,并将姓氏改为 B。

两个节点都已存在于数据库中,表明了顶级 update 操作的用例。

由于他们的节点通过 friends 关系相关联,您可以在同一次变更中执行这两项更改,从顶级 update 操作开始。

注意:数据库中只包含一个从 Bob 指向 Camilla 的 friends 关系。这意味着必须在 Bob 的节点上执行顶级 update 操作,因为 Camilla 的节点不符合嵌套 update 操作的过滤条件。

阅读queryDirection 和变更部分,了解 @relationship 指令的 queryDirection 参数如何影响此注意事项。

示例 6. 带有嵌套 update 操作的顶级 update
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
    }
  }
}

删除与断开连接

场景: Alice 通知聚会上的每个人她要搬走了。Bob 不喜欢 Charlie 听到消息时的表现,想和他断绝关系。

Alice 的节点需要从数据库中删除,表明了顶级 delete 操作的用例。

但是,场景中的两个动作都有 Bob 参与,因此一个有效的替代方案是在 Bob 的节点上进行顶级 update 操作,并使用嵌套操作对 Alice 和 Charlie 的节点执行正确的更改。

请注意,这之所以可能,是因为 Bob 与两者都有关系。必须存在从 Bob 到 Alice 的 HAS_FRIEND 类型关系,Alice 节点才能满足嵌套 delete 操作的过滤条件,Charlie 和嵌套 disconnect 操作也是如此。

示例 7. 带有嵌套 disconnect 和 delete 操作的顶级 update
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
    }
  }
}

场景: Bob 和 Camilla 要一起搬走。

两个节点都需要被删除,连同所有与它们相连的关系,这表明了顶级 delete 操作的用例。

由于节点通过 friends 关系相关联,因此可以在同一次变更中执行两次删除,从 delete 操作开始。

就像上面的更新部分一样,有一个注意事项:数据库中只包含一个从 Bob 指向 Camilla 的 friends 关系。这意味着必须在 Bob 的节点上执行顶级 delete 操作,因为 Camilla 的节点不符合嵌套 delete 操作的过滤条件。

让我们探讨在两个方向上运行相同变更的结果

示例 8. 带有匹配 0 个关系的嵌套 delete 操作的顶级 delete
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
  }
}

此查询的结果是删除 Camilla 节点以及 Bob 和 Camilla 之间的关系

{
  "data": {
    "deletePeople": {
      "nodesDeleted": 1,
      "relationshipsDeleted": 1
    }
  }
}
示例 9. 带有匹配正确节点的嵌套 delete 操作的顶级 delete
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
  }
}

此查询的结果是删除 Bob 和 Camilla 两个节点以及它们之间的关系

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

阅读queryDirection 和变更部分,了解 @relationship 指令的 queryDirection 参数如何影响这些变更的结果。

queryDirection 和变更

@relationship 指令的 queryDirection 参数会影响数据库查询。

就 GraphQL 操作而言,这意味着查询和变更都会受到其值的影响。

执行变更时,首先需要从数据库中查询变更输入中指定的节点和关系。

正是在这个阶段,queryDirection 参数变得相关,因为它决定了在执行变更时,匹配节点与其相关节点之间的关系是否以有向方式被考虑。

重申上面的例子,将从 Bob 到 Camilla 的 friends 关系更改为 acquaintances 关系,将会如下改变变更的结果

示例 10. 在无向关系上进行带有嵌套 delete 操作的顶级 delete

设置

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

变更

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

此查询的结果是删除 Bob 和 Camilla 两个节点以及它们之间的关系

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