单关系与基数考量

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

单关系是指在关系任一侧建模为非列表类型字段的关系。如果两侧均为非列表类型,则该关系为一对一关系;如果一侧为非列表类型而另一侧为列表类型,则该关系为一对多关系。

Neo4j 数据库目前不支持关系基数约束。但是,一对一和一对多关系可以使用 GraphQL 模式进行建模,并且 Neo4j GraphQL 库支持这些建模。

此功能的用户应注意,数据库中无法保证仅存在一个关系,这可能会导致非确定性行为。唯一的保证是,在跨单关系进行查询时只会返回一个结果,但该结果可能是任何一个相关节点

数据模型

以以下图表为例,其中一个 Person(人)可以执导多部电影,但一个 Movie(电影)只能有一位导演。

从概念上讲,这是一种一对多关系,单关系由 Movie 类型上的 DIRECTED(执导)关系字段表示。

2 relationship types
图 1. 通过两种不同关系类型 ACTED_IN(参演)和 DIRECTED(执导)连接到 Person 节点的 Movie 节点模型。
在 Person 和 Movie 之间定义具有 DIRECTED 类型的一对多关系的节点类型
type Person @node {
    name: String!
    directed: [Movie!]! @relationship(type: "DIRECTED", direction: OUT)
}

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

查询单关系

如果存在关系,查询单关系将返回单个节点的数据,否则返回 null。如果数据库包含多个相同类型的关系(这是 Neo4j 基数限制可能导致的后果),则仅返回一个节点。详见基数限制

示例 1. 按标题获取电影及其通过 DIRECTED 关系关联的 Person
query {
  movies(where: { title: { eq: "No Country for Old Men" } }) {
    title
    director {
      name
    }
  }
}

按单关系过滤

单关系(非空类型)可用作查询中 where 参数的过滤器。

示例 2. 通过 DIRECTED 关系按导演姓名过滤 Movie 节点
query {
  movies(where: { director: { name: { eq: "Joel Coen" } } }) {
    title
  }
}

修改单关系

Neo4j GraphQL 库为关系字段提供的操作中,只有一部分可用于单关系(非空类型)。

目前,单关系仅支持创建(create)和删除(delete)操作。

创建单关系

可以使用 create 嵌套变更操作在单关系的单侧创建节点,并将其连接到源节点。

示例 3. 创建一个 Movie 节点,并通过 DIRECTED 关系将其连接到一个内联创建的 Person 节点

以下查询将同时创建 Movie 节点和 Person 节点,并通过 DIRECTED 关系将它们连接起来。

mutation {
  createMovies(input: [{
    title: "No Country for Old Men",
    released: 2007,
    director: { create: { node: { name: "Joel Coen" } } }
}]){
    movies {
        title
        director {
            name
        }
    }
  }
}

然而,嵌套的 connect 变更不可用。因此,如果单关系单侧的节点已经存在,则必须从关系的“多”侧创建该关系。这可能意味着需要进行第二次操作。

示例 4. 创建一个 Movie 节点,并通过 DIRECTED 关系将其连接到现有的 Person 节点

以下查询将创建 Movie 节点。

mutation {
  createMovies(input: [{
    title: "No Country for Old Men",
    released: 2007
}]){
    movies {
        title
    }
  }
}

然后,第二个查询可以通过 DIRECTED 关系连接这两个节点。

mutation {
  updatePerson(
    where: { name: { eq: "Joel Coen" } }
    update: {
        directed: {
            connect: {
                where: { node: { title: { eq: "No Country for Old Men" } } }
            }
        }
    }
  ){
    info {
        relationshipsCreated
    }
  }
}

删除单关系

与创建操作类似,在删除源节点时,可以将删除单关系单侧的节点作为嵌套操作执行。

示例 5. 删除通过 DIRECTED 关系连接的 Movie 和 Person 节点

以下查询会删除 Movie 节点,如果通过遍历 DIRECTED 关系找到任何相关节点,也会一并删除 Person 节点。

mutation {
  deleteMovies(
    where: { title: { eq: "No Country for Old Men" } },
    delete: {
        director: {
            where: { node: { name: { eq: "Joel Coen" } } }
        }
    }
  ){
    nodesDeleted
    relationshipsDeleted
  }
}

然而,嵌套的 disconnect 变更不可用。因此,如果目的是删除关系,则必须从关系的“多”侧执行断开连接操作。

示例 6. 删除 Movie 和 Person 节点之间现有的 DIRECTED 关系
mutation {
  updatePerson(
    where: { name: { eq: "Joel Coen" } }
    update: {
        directed: [{
            disconnect: {
                where: { node: { title: { eq: "No Country for Old Men" } } },
            }
        }]
    }
  ){
    info {
        relationshipsDeleted
    }
  }
}

基数限制

确切的一个关系

分析上述模型,可以观察到 Movie 不可能没有导演。因此,理想情况下,您希望将 director 字段设为非空(non-nullable)。

由于目前 Neo4j 数据库缺乏关系基数约束,因此无法强制执行这种“确切一个”关系的基数,这就是为什么所有单关系字段都必须是可为空的。

最多一个关系

可为空的单关系字段建模了这些类型的任意两个节点之间最多存在一个关系的逻辑。

在多个界面通过不同 API 或直接数据库访问来访问同一个 Neo4j 数据库的情况下,GraphQL API 建模为两个类型之间的单关系,最终在数据库中可能匹配到多个关系。这是由于目前缺乏关系基数约束所致。

即使在同一个 GraphQL API 界面内,一对多关系也可能最终在数据库中匹配到多个关系。

考虑以下示例数据

2Person Movie single relationship
图 2. 数据库中未连接的节点(左);运行以下创建和更新操作后的数据库状态(右)
示例 7. 创建一个 Movie 节点,并通过 DIRECTED 关系将其连接到两个 Person 节点

以下查询会将 Movie 节点通过 DIRECTED 关系连接到两个 Person 节点。

mutation {
  updatePerson(
    where: { name: { contains: "Coen" } }
    update: {
        directed: {
            connect: {
                where: { node: { title: { eq: "No Country for Old Men" } } }
            }
        }
    }
  ){
    info {
        relationshipsCreated
    }
  }
}

由于 GraphQL 模式中关系的类型,查询 Movies 及其 director 关系字段时仅返回一个对象。如果同一两个节点之间存在多个相同类型的关系,则仅返回一个节点。

返回的节点是非确定性的,这意味着它可能是通过该关系连接的任何一个节点。

示例 8. 按标题获取电影及其通过 DIRECTED 关系关联的 Person
query {
  movies(where: { title: { eq: "No Country for Old Men" } }) {
    title
    director {
      name
    }
  }
}

假设使用本页面的示例数据,上述查询的结果是以下两个选项中的任意一个

潜在查询结果
const resultWithJoelCoen = {
    data: {
         movies: [
                {
                    title: "No Country for Old Men",
                    director: { name: "Joel Coen" },
                },
            ],
    },
};
同样的潜在查询结果
const resultWithEthanCoen = {
    data: {
         movies: [
                {
                    title: "No Country for Old Men",
                    director: { name: "Ethan Coen" },
                },
            ],
    },
};