添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
·  阅读 3.neo4j-graphql的基本使用

GraphQL 后端实现通常会遇到一系列对性能和开发人员生产力产生负面影响的问题。 我们之前已经确定了其中的几个问题(例如“n+1 查询问题”),在本文中,我们将更深入地研究出现的常见问题,并讨论如何使用 GraphQL 的数据库集成来缓解这些问题 使构建由数据库支持的高效 GraphQL API 变得更加容易。

具体来说,我们着眼于使用 neo4j-graphql.js,这是一个 Node.js 库,旨在与 JavaScript GraphQL 实现(例如 Apollo Server)一起构建由 Neo4j 支持的 GraphQL API。 neo4j-graphql.js 允许我们从类型定义生成 GraphQL API,从 GraphQL 驱动数据库数据模型,自动生成用于数据获取和操作的解析器,包括复杂的过滤、排序和分页。 neo4j-graphql.js 还支持添加自定义逻辑。 我们将研究使用 neo4j-graphql.js 将我们的业务评论 GraphQL API 与 Neo4j 集成,为我们的 API 添加一个持久层。在对 neo4j-graphql.js 的初步了解中,我们专注于查询现有数据,使用上一章中使用的 Neo4j 中的示例数据集。我们将探索创建和更新数据(GraphQL 操作),以及更复杂的 GraphQL 查询语义,例如接口和片段。下图 显示了 neo4j-graphql.js 如何适应我们应用程序的更大架构。 neo4j-graphql.js 的目标是让构建由 Neo4j 支持的 API 变得容易。

1 GraphQL 的公共问题

在构建 GraphQL API 时,开发人员可能会面临两种常见问题:性能不佳和编写大量可能影响开发人员生产力的样板代码。

1.1 糟糕的性能和N+1查询问题

我们之前已经描述了 N+1 查询问题。 由于调用 GraphQL 解析器的嵌套方式,通常需要多个数据库请求来解析来自数据层的 GraphQL 查询。 例如,假设一个查询按名称搜索企业以及每个企业的所有评论。 一个简单的实现会首先在数据库中查询与搜索词组匹配的所有企业。 然后,对于每个匹配的企业,他们会向数据库发送一个额外的查询,以查找该企业的任何评论。 对数据库的每次查询都会导致网络和查询延迟,这会显着影响性能。 一个常见的解决方案是使用称为 DataLoader 的缓存和批处理模式。 这样可以缓解部分性能问题; 但是,它仍然可能需要多个数据库请求,并且不能在所有情况下都使用,例如当对象的 ID 未知时。

1.2 模版和开发人员生产力

术语模版用于描述为完成常见任务而编写的重复代码。 在实现 GraphQL API 的情况下,通常需要编写模版代码来实现解析器中的数据获取逻辑。 这会对开发人员的生产力产生负面影响,减慢开发速度,因为开发人员需要为每种类型和字段编写简单的数据获取逻辑,而不是专注于其应用程序的关键组件。 在我们的业务评论应用程序的上下文中,这意味着手动编写在数据库中按名称查找业务的逻辑,以及查找与每个业务相关联的评论以及与每个评论相关联的每个用户等等,直到我们手动 定义了获取 GraphQL 模式的所有字段的逻辑。

2 介绍 GraphQL 的集成

GraphQL 数据库集成是一类工具,可以构建与数据库交互的 GraphQL API。其中一些工具具有不同的功能集和集成级别——在本文中,我们关注的是 neo4j-graphql.js。然而,总的来说,这些 GraphQL “引擎”的目标是通过减少样板文件和解决数据获取性能问题,以一致的方式解决之前发现的常见 GraphQL 问题。 在本章的其余部分,我们将重点关注使用 neo4j-graphql.js 构建由 Neo4j 支持的 GraphQL API。需要注意的是,我们的 GraphQL API 充当客户端和数据库之间的一层;我们不想直接从客户端查询我们的数据库。 API 层提供了一个重要的功能,我们可以在其中实现我们不想向客户端公开的功能,例如授权和自定义逻辑。此外,由于 GraphQL 是一种 API 查询语言(不是数据库查询语言),它缺乏我们在数据库查询语言中所期望的许多语义(例如聚合和投影)。

3 neo4j-graphql.js library

Neo4j-graphql.js 是一个 Node.js 库,可与任何 JavaScript GraphQL 实现(例如 GraphQL.js 和 Apollo Server)一起使用,旨在尽可能轻松地构建由 Neo4j 数据库支持的 GraphQL API。 neo4j-graphql.js 的两个主要功能是schema扩充和 GraphQL 转译

模式扩充过程采用 GraphQL 类型定义,并为定义的类型生成具有 CRUD(创建、读取、更新、删除)操作的 GraphQL API。在 GraphQL 语义中,这包括向模式添加 Query 和 Mutation 类型,并为这些查询和操作生成解析器。生成的 API 包括对过滤、排序、分页和本地数据库类型(例如空间和时间类型)的支持,而无需在类型定义中手动定义这些。这个过程的结果是一个 GraphQL 可执行模式对象,然后可以将其传递给 GraphQL 服务器实现,例如 Apollo Server,以服务 API 并处理网络和 GraphQL 执行过程。模式增强过程消除了为数据获取和映射 GraphQL 和数据库模式编写样板代码的需要。 GraphQL 转译过程发生在查询时。当收到 GraphQL 请求时,会生成一个可以解析请求的 Cypher 查询并将其发送到数据库。生成单个数据库查询解决了 n+1 查询问题,确保每个 GraphQL 请求仅往返一次数据库。

3.1 项目配置

我们将通过为 Neo4j 创建一个新的 GraphQL API 来探索 neo4j-graphql.js 的特性。 我们将首先创建一个新的 Node.js 项目,该项目使用 neo4j-graphql.js 和 Neo4j JavaScript 驱动程序从 Neo4j 获取数据。 然后我们将探索 neo4j-graphql.js 的各种功能,并在我们继续进行时添加其他代码。

Neo4j

NEO4J 首先,确保 Neo4j 实例正在运行(您可以使用 Neo4j Desktop、Neo4j Sandbox 或 Neo4j Aura,我们假设使用 Neo4j Desktop)。 如果使用 Neo4j Desktop,则需要安装 APOC 标准库插件。 如果使用 Neo4j Sandbox 或 Neo4j Aura,默认包含 APOC。 要在 Neo4j Desktop 中安装 APOC,请单击项目中的“Plugins”选项卡,然后在可用插件列表中查找 APOC,然后单击“Install”。 接下来,通过运行下面的 Cypher 语句来确保 Neo4j 数据库是空的。

MATCH (a) DETACH DELETE a;

加载实例数据集

:play grandstack

这会将示例数据集加载到 Neo4j 中,我们将使用它作为 GraphQL API 的基础。接下来,我们可以通过运行以下列表中的命令来稍微探索数据,这将为我们提供示例数据集中包含的数据的可视化概览

然后我们执行

CALL db.schema.visualization();

可视化图数据库的schema

我们看到我们有四个节点标签:Business、Review、Category 和 User,由三种关系类型连接:IN_CATEGORY(将企业连接到它们所属的类别)、REVIEWS(将评论连接到企业)和 WROTE (将用户连接到他们撰写的评论)。

CALL db.schema.nodeTypeProperties()

此命令呈现一个表格,向我们显示属性名称、它们的类型以及是否在该标签的所有节点上找到它们。

稍后我们将在构建描述此图的 GraphQL 类型定义时使用此表。

现在我们的 Neo4j 数据库已经加载了我们的示例数据集,让我们为我们的 GraphQL API 设置一个新的 node.js 项目:

首先新建文件夹并且初始化npm init -y

npm install neo4j-graphql-js apollo-server neo4j-driver安装依赖

现在创建一个新文件 index.js,并且添加初始代码。

const { ApolloServer } = require("apollo-server");
const neo4j = require("neo4j-driver");
const { makeAugmentedSchema } = require("neo4j-graphql-js");
const typeDefs = ``  // Put your type definitions here
// makeAugmentedSchema generates resolvers for our type definitions.
const schema = makeAugmentedSchema({
    typeDefs,
// Our generated GraphQL schema is passed to Apollo Server.
const server = new ApolloServer({
    schema
// start the GraphQL server
server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);

这是我们的 GraphQL API 应用程序代码的基本结构。 我们可以使用 node 命令在命令行上运行它:node index.js

但是,我们很快就会看到一条错误消息,说我们没有提供 GraphQL 类型定义。

我们必须向定义 GraphQL API 的 makeAugmentedSchema 提供 GraphQL 类型定义或 GraphQL 模式对象,因此下一步是填写我们的 GraphQL 类型定义。

现在我们搭建好了基础的开发环境

3.2 定义类型 生成schema

遵循前面描述的 GraphQL-First 方法,我们的 GraphQL 类型定义驱动 API 规范。 在这种情况下,我们知道要公开哪些数据(我们在 Neo4j 中加载的示例数据集),因此我们可以参考上面的节点属性表,并在创建 GraphQL 类型定义时应用一个简单的规则:节点标签变成类型, 将节点属性作为字段。 我们还需要在 GraphQL 类型定义中定义关系字段。 让我们先看看完整的类型定义,然后在下面的清单中探索我们如何定义关系字段。

在上面的index.js中定义type

const typeDefs = `
  type Business {
    businessId: ID!
    name: String!
    city: String!
    state: String!
    address: String!
    location: Point!
    reviews: [Review] @relation(name: "REVIEWS", direction: "IN")
    categories: [Category] @relation(name: "IN_CATEGORY", direction: "OUT")
  type User {
    userID: ID!
    name: String!
    reviews: [Review] @relation(name: "WROTE", direction: "OUT")
  type Review {
    reviewId: ID!
    stars: Float!
    date: Date!
    text: String
    user: User @relation(name: "WROTE", direction: "IN")
    business: Business @relation(name: "REVIEWS", direction: "OUT")
  type Category {
    name: String!
    businesses: [Business] @relation(name: "IN_CATEGORY", direction: "IN")

@relation 命令

在 Neo4j 使用的属性图模型中,每个关系都有一个方向和类型。 为了在 GraphQL 中表示这一点,我们使用 GraphQL 模式指令,特别是 @relation schema指令。

指令就像我们的 GraphQL 类型定义中的注解。 它是一个以 @ 字符开头的标识符,然后可以选择包含命名参数的列表。 Schema 指令是 GraphQL 的内置扩展机制,表示服务器上的一些自定义逻辑。

当使用@relation 指令定义关系字段时,name 参数表示存储在 Neo4j 中的关系类型,direction 参数表示关系方向。

除了模式指令之外,指令还可以在 GraphQL 查询中用于指示特定行为。 当我们探索 GraphQL 客户端时,我们将看到查询指令的示例。

3.3 data fetching

我们 GraphQL 模式中自动生成的解析器需要使用 Neo4j JavaScript 驱动程序访问我们的 Neo4j 数据库,并且它们希望将驱动程序实例注入到传递给每个解析器的上下文对象中。 按照惯例,驱动程序被注入到上下文对象中的关键驱动程序下,如下面所示。

创建一个Neo4j driver 实例

// letmein 使用数据库的密码代替,创建一个driver实例
const driver = neo4j.driver(
  "bolt://localhost:7687",
  neo4j.auth.basic("neo4j", "letmein")
// 注入到上下文对象中
const server = new ApolloServer({
  schema,
  context: { driver }

3.4 配置生成的API

我们提到 schema 扩充过程为类型定义中定义的每种类型添加查询和操作。 我们可以配置生成的 API,完全禁用查询或操作,或指定排除某些类型。

// 禁用生成的 mutation
const schema = makeAugmentedSchema({
  typeDefs,
  resolvers,
  config: {
      mutation: false
//禁用特殊的 types
const schema = makeAugmentedSchema({
  typeDefs,
  resolvers,
  config: {
      mutation: false,
      query: {
          exclude: ["MySecretType"]
} });

现在可以运行我们配置的API应用

node index.js

我们会看到控制台打印输出 4000 端口启动服务

因为我们最初只关注查询 API,所以让我们在下面的清单中禁用所有生成的操作。 在 Web 浏览器中导航到 http://localhost:4000,应该会看到熟悉的 GraphQL Playground 界面。 在 GraphQL 中打开“Docs”选项卡以查看生成的 API。 花几分钟浏览 Query 字段描述,你会注意到已将参数添加到类型中,用于排序、分页和过滤等内容。

4 基本查询操作

现在我们的 GraphQL 服务器由 Apollo Server 和 neo4j-graphql.js 启动并运行,让我们开始使用 GraphQL Playground 查询我们的 API。 查看 GraphQL Playground 中的 Docs 选项卡,我们可以看到 API 入口点(在 GraphQL 用语来说,每个查询类型字段都是 API 的入口点)可供我们使用:业务、用户、评论、类别,一个对应于我们类型定义中定义的每种类型。 让我们从查询所有企业开始,只返回名称字段。

query

Business {

这些数据是从我们的 Neo4j 实例中为我们获取的,我们甚至不需要编写任何解析器! 如果我们检查终端中的控制台输出,我们可以在以下清单中看到生成的 Cypher 查询记录到终端。

对应Cypher

MATCH (`business`:`Business`) RETURN `business` { .name } AS `business`

我们可以向 GraphQL 查询添加额外的字段,这些字段将被添加到生成的 Cypher 查询中,只返回所需的数据。 例如,以下 GraphQL 查询添加了企业地址以及名称字段,如下面的清单所示。

Business {   address

对应Cypher

MATCH (`business`:`Business`) RETURN `business` { .name , .address } AS `business`

5 排序和分页

每个输入类型字段包括 first、offset 和 orderBy 参数以启用排序和分页。 我们搜索前三个企业,按名称字段的值排序。

Business(first: 3, orderBy: name_asc) {

为每种类型生成排序枚举,为每个字段提供升序和降序选项。

为每个business 类型生成排序枚举

enum _BusinessOrdering {
          businessId_asc
          businessId_desc
          name_asc
          name_desc
          city_asc
          city_desc
          state_asc
          state_desc
          address_asc
          address_desc
          _id_asc
          _id_desc

如果我们切换到终端,我们可以看到从我们的 GraphQL 查询生成的 Cypher 查询,它现在包括映射到我们的 firstORDER BY GraphQL 参数的 ORDER BY 和 LIMIT 子句。 排序和限制是在数据库中执行的,而不是在客户端中,所以只有必要的数据从客户端返回

MATCH (`business`:`Business`) WITH `business` ORDER BY business.name ASC
        RETURN `business` { .name } AS `business` LIMIT toInteger($first)

6 嵌套查询

Cypher 可以在我们的 GraphQL 查询中轻松表达图遍历的类型,neo4j-graphql.js 能够为任意 GraphQL 请求(包括嵌套查询)生成等效的 Cypher 查询。 在下面的清单中,我们从businesses遍历到它们的categories

进行嵌套查询

Business(first: 3, orderBy: name_asc) { categories {

通过添加一个带有关联输入的过滤器参数来公开过滤器功能,该过滤器参数基于公开过滤条件的 GraphQL 类型定义。

7.1 过滤参数

我们使用过滤器参数来搜索包含“Brew”的企业名称。

Business(filter: { name_contains: "Brew" }) { address

我们的结果现在显示符合过滤条件的企业,并且仅返回名称中包含字符串“Brew”的企业。

7.2 嵌套过滤

要根据应用于根的嵌套字段的结果进行过滤,我们可以嵌套我们的过滤器参数。 在以下列表中,我们搜索名称包含“Brew”并且至少有一条评价至少为 4.75 的企业。

Business(   filter: { name_contains: "Brew", reviews_some: { stars_gte: 4.75 } } ){   address

7.3 逻辑操作符 AND , OR

过滤器可以包含在逻辑运算符 OR 和 AND 中。 例如,我们可以通过在过滤器参数中使用 OR 运算符来搜索 Coffee 或 Breakfast 类别中的企业

Business( filter: {   OR: [     { categories_some: { name: "Coffee" } }     { categories_some: { name: "Breakfast" } }   address   categories { name }

此 GraphQL 查询生成与咖啡或早餐类别相关的企业。

7.4 筛选过滤

过滤器也可以在整个选择集中使用,以在选择级别应用过滤器。 例如,假设我们要查找所有 Coffee 或 Breakfast 商家,但只查看包含短语“breakfast sandwich”的评论。

Business(   filter: {     OR: [       { categories_some: { name: "Coffee" } }       { categories_some: { name: "Breakfast" } }   address   reviews(filter: { text_contains: "breakfast sandwich" }) {     stars

由于在评论选择中应用了过滤器,因此结果中仍会显示没有包含“早餐三明治”短语的评论的商家; 但是,仅显示包含该短语的评论。

8 添加自定义逻辑

我们已经看到了 neo4j-graphql.js 创建的基本查询操作。 通常,我们希望将自定义逻辑添加到我们的 API。 例如,我们可能想要计算最受欢迎的业务或向用户推荐业务。 使用 neo4j-graphql.js 向 API 添加自定义逻辑有两种选择:1)@cypher schema指令,2)通过实现自定义resolvers。

8.1 @cypher

我们通过 @cypher 指令通过 GraphQL 公开 Cypher。 使用 @cypher 指令注释架构中的字段,以将该查询的结果映射到带注释的 GraphQL 字段。 @cypher 指令采用单个参数语句,即 Cypher 语句。 参数在运行时传递到此查询中,包括当前解析的节点以及 GraphQL 类型定义中定义的任何字段级参数。

我们通过 @cypher 指令通过 GraphQL 公开 Cypher。 使用 @cypher 指令注释架构中的字段,以将该查询的结果映射到带注释的 GraphQL 字段。 @cypher 指令采用单个参数语句,即 Cypher 语句。 参数在运行时传递到此查询中,包括当前解析的节点以及 GraphQL 类型定义中定义的任何字段级参数。

注意 @cypher 指令功能需要使用 APOC 标准库插件。 确保按照本章“项目设置”部分中的步骤安装 APOC。

我们可以使用 @cypher 指令来定义一个自定义标量字段,在我们的模式中定义一个计算字段。 这里我们在 Business 类型中添加了一个 averageStars 字段,它使用这个变量来计算该商家所有评论的平均星级

type Business {
    businessId: ID!
    averageStars: Float! @cypher(statement:"MATCH (this)<-[:REVIEWS]-
     (r:Review) RETURN avg(r.stars)")
    name: String!
    city: String!
    state: String!
    address: String!
    location: Point!
    reviews: [Review] @relation(name: "REVIEWS", direction: "IN") categories: [Category] @relation(name: "IN_CATEGORY", direction: "OUT")

8.2 实现自定义 resolvrs

虽然 @cypher 指令是添加自定义逻辑的一种方式,但在某些情况下,我们可能需要实现自定义解析器,以实现无法用 Cypher 表达的逻辑。例如,我们可能需要从另一个系统获取数据,或应用自定义验证规则。在这些情况下,我们可以实现自定义解析器并将其附加到 GraphQL 模式,以便调用解析器来解析我们的自定义字段,而不是依赖 neo4j-graphql.js 生成的 Cypher 查询来解析该字段。 在我们的示例中,假设有一个外部系统可用于确定企业当前的等待时间。我们想在架构中的业务类型中添加一个额外的 waitTime 字段,并为该字段实现解析器逻辑以使用此外部系统。 为此,我们首先将字段添加到我们的模式中,添加 @neo4j_ignore 指令以确保从生成的 Cypher 查询中排除该字段。这是我们告诉 neo4j-graphql.js 自定义解析器将负责解析此字段的方式,我们不希望它自动从数据库中获取

type Business {
    businessId: ID!
    waitTime: Int! @neo4j_ignore
    averageStars: Float!
    @cypher(
    statement: "MATCH (this)<-[:REVIEWS]-(r:Review) RETURN avg(r.stars)"
    name: String!
    city: String!
    state: String!
    address: String!
    location: Point!
    reviews: [Review] @relation(name: "REVIEWS", direction: "IN") categories: [Category] @relation(name: "IN_CATEGORY", direction: "OUT")

\

分类:
前端
标签: