为什么 GraphQL 查询 return 为空?

Why does a GraphQL query return null?

我有一个 graphql/apollo-server/graphql-yoga 端点。此端点公开从数据库(或 REST 端点或其他服务)返回的数据。

我知道我的数据源正在返回正确的数据 -- 如果我在我的解析器中记录对数据源的调用结果,我可以看到返回的数据。但是,我的 GraphQL 字段总是解析为空。

如果我将字段设置为非空,我会在响应的 errors 数组中看到以下错误:

Cannot return null for non-nullable field

为什么 GraphQL 不返回数据?

您的一个或多个字段解析为空的常见原因有两个:1) return在您的解析器中以错误的形式输入数据;和 2) 没有正确使用 Promises。

注意: 如果您看到以下错误:

Cannot return null for non-nullable field

根本问题是您的字段 returning null。您仍然可以按照下面概述的步骤尝试解决此错误。

以下示例将引用此简单模式:

type Query {
  post(id: ID): Post
  posts: [Post]
}

type Post {
  id: ID
  title: String
  body: String
}

以错误的形式返回数据

我们的模式以及请求的查询在我们端点 return 的响应中定义了 data 对象的 "shape"。通过形状,我们指的是对象具有哪些属性,以及这些属性的值是标量值、其他对象还是对象或标量数组。

与模式定义总响应的形状相同,单个字段的 type 定义该字段值的形状。我们 return 在我们的解析器中的数据形状必须同样匹配这个预期的形状。否则,我们的响应中经常会出现意想不到的空值。

不过,在我们深入研究具体示例之前,了解 GraphQL 如何解析字段很重要。

了解默认解析器行为

虽然您当然 可以 为模式中的每个字段编写一个解析器,但通常没有必要,因为 GraphQL.js 在您不提供解析器时使用默认解析器.

在高层次上,默认解析器所做的很简单:它查看 parent 字段解析到的值,如果该值是 JavaScript 对象,它会在该对象上查找与要解析的字段 同名 的 属性。如果它发现 属性,它会解析为 属性 的值。否则,它解析为 null。

假设在 post 字段的解析器中,我们 return 值 { title: 'My First Post', bod: 'Hello World!' }。如果我们不为 Post 类型的任何字段编写解析器,我们仍然可以请求 post:

query {
  post {
    id
    title
    body
  }
}

我们的回应是

{
  "data": {
    "post" {
      "id": null,
      "title": "My First Post",
      "body": null,
    }
  }
}

尽管我们没有为它提供解析器,但 title 字段已解析,因为默认解析器完成了繁重的工作——它看到有一个名为 属性 的 title 在对象上,父字段(在本例中为 post)解析为,因此它只是解析为 属性 的值。 id 字段解析为空,因为我们在 post 解析器中 returned 的对象没有 id 属性。由于拼写错误,body 字段也解析为空 - 我们有一个名为 bod 的 属性 而不是 body!

Pro tip: If bod is not a typo but what an API or database actually returns, we can always write a resolver for the body field to match our schema. For example: (parent) => parent.bod

要牢记的一件重要事情是,在JavaScript、中几乎所有东西都是对象。因此,如果 post 字段解析为字符串或数字,Post 类型上每个字段的默认解析器仍将尝试在父对象上找到适当命名的 属性 ,不可避免地会失败并且 return 为空。如果一个字段有一个对象类型,但你 return 它的解析器中的对象不是对象(如字符串或数组),你将不会看到任何关于类型不匹配的错误,但该字段的子字段将不可避免地解析为空。

常见场景 #1:包装响应

如果我们正在为 post 查询编写解析器,我们可能会从其他端点获取我们的代码,如下所示:

function post (root, args) {
  // axios
  return axios.get(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.data);

  // fetch
  return fetch(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.json());

  // request-promise-native
  return request({
    uri: `http://SOME_URL/posts/${args.id}`,
    json: true
  });
}

post 字段的类型为 Post,因此我们的解析器应该 return 具有 idtitle 和 [=40 等属性的对象=].如果这就是我们的 API return,那我们就准备好了。 但是,响应实际上是一个包含额外元数据的对象是很常见的。所以我们实际从端点返回的对象可能看起来像这样:

{
  "status": 200,
  "result": {
    "id": 1,
    "title": "My First Post",
    "body": "Hello world!"
  },
}

在这种情况下,我们不能只 return 响应 as-is 并期望默认解析器正常工作,因为我们 returning 的对象不具有我们需要的 idtitlebody 属性。我们的解析器不需要做类似的事情:

function post (root, args) {
  // axios
  return axios.get(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.data.result);

  // fetch
  return fetch(`http://SOME_URL/posts/${args.id}`)
    .then(res => res.json())
    .then(data => data.result);

  // request-promise-native
  return request({
    uri: `http://SOME_URL/posts/${args.id}`,
    json: true
  })
    .then(res => res.result);
}

注意:上面的例子是从另一个端点获取数据;然而,当直接使用数据库驱动程序(而不是使用 ORM)时,这种包装响应也很常见!例如,如果您使用 node-postgres,您将得到一个 Result 对象,其中包含 rowsfieldsrowCount 和 [=61] 等属性=].在将其 return 放入解析器之前,您需要从该响应中提取适当的数据。

常见场景 #2:数组而不是对象

如果我们从数据库中获取 post 会怎么样,我们的解析器可能看起来像这样:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
}

其中 Post 是我们通过上下文注入的模型。如果我们使用 sequelize,我们可能会调用 findAllmongoosetypeormfind。这些方法的共同点是,虽然它们允许我们指定一个 WHERE 条件,他们 return 的 Promise 仍然解析为一个数组而不是单个对象 。虽然您的数据库中可能只有一个 post 具有特定 ID,但当您调用这些方法之一时,它仍然包含在一个数组中。因为 Array 仍然是 Object,所以 GraphQL 不会将 post 字段解析为 null。但它 将所有子字段解析为 null,因为它无法在数组中找到适当命名的属性。

您只需抓住数组中的第一项并 return 在您的解析器中对其进行处理即可轻松解决此问题:

function post(root, args, context) {
  return context.Post.find({ where: { id: args.id } })
    .then(posts => posts[0])
}

如果您要从另一个 API 获取数据,这通常是唯一的选择。另一方面,如果您使用的是 ORM,通常可以使用一种不同的方法(如 findOne),该方法将显式 return 仅来自数据库的一行(如果它是空的,则为 null)不存在)。

function post(root, args, context) {
  return context.Post.findOne({ where: { id: args.id } })
}

关于 INSERTUPDATE 调用的特别说明:我们通常期望将行或模型实例插入或更新到 return 插入或更新的行。通常他们会这样做,但有些方法不会。例如,sequelizeupsert 方法解析为布尔值,或者由更新的记录和布尔值组成的元组(如果 returning 选项设置为 true)。 mongoosefindOneAndUpdate 解析为具有 value 属性 的对象,其中包含修改后的行。查阅您的 ORM 文档并在 return 将结果放入您的解析器之前适当地解析结果。

常见场景 #3:对象而不是数组

在我们的架构中,posts 字段的类型是 PostList,这意味着它的解析器需要 return 对象数组(或解决为一的承诺)。我们可能会像这样获取 posts:

function posts (root, args) {
  return fetch('http://SOME_URL/posts')
    .then(res => res.json())
}

然而,来自我们 API 的实际响应可能是一个对象,它包装了 posts 的数组:

{
  "count": 10,
  "next": "http://SOME_URL/posts/?page=2",
  "previous": null,
  "results": [
    {
      "id": 1,
      "title": "My First Post",
      "body" "Hello World!"
    },
    ...
  ]
}

我们不能在我们的解析器中 return 这个对象,因为 GraphQL 需要一个数组。如果我们这样做,该字段将解析为 null,我们将在我们的响应中看到一个错误,如:

Expected Iterable, but did not find one for field Query.posts.

与上述两种情况不同,在这种情况下,GraphQL 能够显式检查我们在解析器中 return 的值的类型,如果它不是像数组那样的 Iterable 就会抛出。

就像我们在第一个场景中讨论的那样,为了修复这个错误,我们必须将响应转换为合适的形状,例如:

function posts (root, args) {
  return fetch('http://SOME_URL/posts')
    .then(res => res.json())
    .then(data => data.results)
}

没有正确使用 Promise

GraphQL.js 在底层使用了 Promise API。因此,解析器可以 return 一些值(如 { id: 1, title: 'Hello!' })或者它可以 return 一个将 解析 到那个值的 Promise。对于具有 List 类型的字段,您还可以 return Promise 数组。如果 Promise 拒绝,该字段将 return 为空,并且适当的错误将添加到响应中的 errors 数组。如果字段具有对象类型,则 Promise 解析为的值将作为 父值 传递给任何子字段的解析器。

一个 Promise 是一个 "object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value." 接下来的几个场景概述了在解析器内部处理 Promises 时遇到的一些常见陷阱。但是,如果您不熟悉 Promises 和更新的 async/await 语法,强烈建议您花一些时间阅读基础知识。

注意:接下来的几个例子参考了一个getPost函数。这个函数的实现细节并不重要——它只是一个 return 是一个 Promise 的函数,它将解析为一个 post 对象。

常见情况 #4:不返回值

post 字段的有效解析器可能如下所示:

function post(root, args) {
  return getPost(args.id)
}

getPosts return 是一个承诺,我们正在 return 实现这个承诺。无论 Promise 解决什么,都将成为我们领域解决的价值。看起来不错!

但是如果我们这样做会发生什么:

function post(root, args) {
  getPost(args.id)
}

我们仍在创建一个将解析为 post 的 Promise。但是,我们没有 returning Promise,因此 GraphQL 不知道它,也不会等待它解析。在没有显式 return 语句的 JavaScript 函数中隐式 return undefined。所以我们的函数创建了一个 Promise,然后立即 returns undefined,导致 GraphQL 为该字段 return null。

如果由 getPost 编辑的 Promise return 被拒绝,我们也不会在响应中看到任何错误 - 因为我们没有 return Promise,底层代码不关心它是解析还是拒绝。事实上,如果 Promise 拒绝,您会在服务器控制台中看到 UnhandledPromiseRejectionWarning

解决这个问题很简单——只需添加 return.

常见场景 #5:未正确链接 Promise

您决定记录对 getPost 的调用结果,因此您将解析器更改为如下所示:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
    })
}

当您 运行 您的查询时,您会看到结果记录在您的控制台中,但 GraphQL 将该字段解析为空。为什么?

当我们在 Promise 上调用 then 时,我们实际上是在获取 Promise 解析的值并 return 创建一个新的 Promise。除了 Promises,你可以把它想象成 Array.mapthen 可以 return 一个值,或者另一个 Promise。在任何一种情况下,return 在 then 中编辑的内容都是 "chained" 到原始 Promise 上。多个 Promise 可以像这样通过使用多个 then 链接在一起。链中的每个 Promise 都按顺序解析,最终值是有效解析为原始 Promise 的值。

在我们上面的示例中,我们 return 在 then 中什么也没有编辑,因此 Promise 解析为 undefined,GraphQL 将其转换为 null。要解决此问题,我们必须 return posts:

function post(root, args) {
  return getPost(args.id)
    .then(post => {
      console.log(post)
      return post // <----
    })
}

如果您有多个 Promise 需要在您的解析器中解析,您必须通过使用 then 和 return 正确的值正确地链接它们。例如,如果我们需要先调用另外两个异步函数(getFoogetBar),然后才能调用getPost,我们可以这样做:

function post(root, args) {
  return getFoo()
    .then(foo => {
      // Do something with foo
      return getBar() // return next Promise in the chain
    })
    .then(bar => {
      // Do something with bar
      return getPost(args.id) // return next Promise in the chain
    })

Pro tip: If you're struggling with correctly chaining Promises, you may find async/await syntax to be cleaner and easier to work with.

常见场景 #6

在 Promises 出现之前,处理异步代码的标准方法是使用回调,或者在异步工作完成后调用的函数。例如,我们可以这样调用 mongoosefindOne 方法:

function post(root, args) {
  return Post.findOne({ where: { id: args.id } }, function (err, post) {
    return post
  })

这里的问题是two-fold。第一,在回调中 returned 的值不用于任何事情(即它不以任何方式传递给底层代码)。第二,当我们使用回调时,Post.findOne 不是 return Promise;它只是 return 未定义。在此示例中,我们的回调将被调用,如果我们记录 post 的值,我们将看到从数据库中编辑的 return 的内容。然而,因为我们没有使用 Promise,GraphQL 不会等待这个回调完成——它接受 return 值(未定义)并使用它。

大多数更流行的库,包括 mongoose 开箱即用地支持 Promises。那些不经常有添加此功能的免费 "wrapper" 库。 使用 GraphQL 解析器时,您应该避免使用利用回调的方法,而是使用 return Promises 的方法。

Pro tip: Libraries that support both callbacks and Promises frequently overload their functions in such a way that if a callback is not provided, the function will return a Promise. Check the library's documentation for details.

如果你绝对必须使用回调,你也可以将回调包装在 Promise 中:

function post(root, args) {
  return new Promise((resolve, reject) => {
    Post.findOne({ where: { id: args.id } }, function (err, post) {
      if (err) {
        reject(err)
      } else {
        resolve(post)
      }
    })
  })

我在 Nest.js 上遇到了同样的问题。

如果你喜欢解决问题。您可以将 {nullable: true} 选项添加到您的 @Query 装饰器。

举个例子。

@Resolver(of => Team)
export class TeamResolver {
  constructor(
    private readonly teamService: TeamService,
    private readonly memberService: MemberService,
  ) {}

  @Query(returns => Team, { name: 'team', nullable: true })
  @UseGuards(GqlAuthGuard)
  async get(@Args('id') id: string) {
    return this.teamService.findOne(id);
  }
}

然后,您可以return空对象进行查询。

如果上述 none 有帮助,并且您有一个全局拦截器将所有响应都封装在 "data" 字段中,您必须为 graphql 禁用此功能,否则 graphql 解析器将转换为空。

这是我对我的案例中的拦截器所做的:

  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    if (context['contextType'] === 'graphql') return next.handle();

    return next
      .handle()
      .pipe(map(data => {
        return {
          data: isObject(data) ? this.transformResponse(data) : data
        };
      }));
  }

以防有人使用 apollo-server-express 并得到空值。

const typeDefs = require('./schema');
const resolvers = require('./resolver');

const server = new ApolloServer({typeDefs,resolvers});

这将 return 值如您所愿。

const withDifferentVarNameSchema = require('./schema');
const withDifferentVarNameResolver= require('./resolver');

const server = new ApolloServer({withDifferentVarNameSchema,withDifferentVarNameResolver});

这将 return 不符合预期。

注意:在创建 Apolloserver 实例时,仅传递 typeDef 和解析器 var 名称。

这里来自 Flutter。 我找不到任何与 flutter 相关的解决方案,因为我的搜索总是把我带到这里,让我在这里添加它。

准确的错误是:

Failure performing sync query to AppSync: [GraphQLResponse.Error{message='Cannot return null for non-nullable type: 'AWSTimestamp' within parent

所以,在我的架构中(在 AppSync 控制台上)我有这个:

type TypeName {
   id: ID!
   ...
   _version: Int!
   _deleted: Boolean
   _lastChangedAt: AWSTimestamp!
   createdAt: AWSDateTime!
   updatedAt: AWSDateTime!
 }

我从字段 _lastChangedAt 中得到错误,因为 AWSTimestamp 不能为空。

我所要做的就是remove the null-check (!) from the field,它就解决了。

现在,我不知道这在长期 运行 中的含义,但如有必要,我会更新此答案。

编辑:我发现这意味着我所做的任何事情,amplify.push 改变都是相反的。只需返回您的 appsync 控制台并在测试时再次更改它。所以这不是一个可持续的解决方案,但我在网上收集到的闲谈表明很快就会有改进来放大颤振。