为什么graphql查询返回null?

4dbbbstv  于 2021-06-10  发布在  Cassandra
关注(0)|答案(1)|浏览(774)

我有一个 graphql / apollo-server / graphql-yoga 终结点。此端点公开从数据库(或rest端点或其他服务)返回的数据。
我知道我的数据源正在返回正确的数据——如果我将调用数据源的结果记录在解析器中,我可以看到返回的数据。但是,我的graphql字段总是解析为null。
如果将字段设置为非null,则在 errors 响应中的数组:
对于不可为null的字段,不能返回null
为什么graphql不返回数据?

mefy6pfw

mefy6pfw1#

字段解析为null有两个常见的原因:1)在解析程序中以错误的形式返回数据;2)没有正确使用承诺。
注意:如果您看到以下错误:
对于不可为null的字段,不能返回null
根本的问题是您的字段返回null。您仍然可以按照下面概述的步骤尝试解决此错误。
以下示例将引用此简单模式:

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

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

返回错误形状的数据

我们的模式和请求的查询一起定义了 data 在端点返回的响应中。所谓形状,我们指的是对象具有什么属性,以及这些属性的“值”是标量值、其他对象还是对象或标量的数组。
与模式定义总响应的形状相同,单个字段的类型定义该字段值的形状。我们在解析器中返回的数据的形状也必须与预期的形状相匹配。如果没有,我们的响应常常会出现意外的空值。
不过,在深入研究具体示例之前,掌握graphql如何解析字段很重要。

了解默认解析程序行为

虽然您当然可以为模式中的每个字段编写一个解析器,但这通常不是必需的,因为graphql.js在您没有提供解析器时使用默认的解析器。
在较高的层次上,默认解析器所做的很简单:它查看父字段解析到的值,如果该值是javascript对象,则在该对象上查找与要解析的字段同名的属性。如果找到该属性,它将解析为该属性的值。否则,它将解析为null。
假设在我们的解析器中 post 字段,则返回值 { title: 'My First Post', bod: 'Hello World!' } . 如果我们不为 Post 类型,我们仍然可以请求 post :

query {
  post {
    id
    title
    body
  }
}

我们的React是

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

这个 title 字段被解析了,尽管我们没有为它提供解析程序,因为默认的解析程序完成了繁重的工作--它看到有一个名为 title 对象上的父字段(在本例中 post )解析为,所以它只是解析为那个属性的值。这个 id 字段解析为null,因为我们在 post 解析程序没有 id 财产。这个 body 字段也被解析为null,因为输入错误--我们有一个名为 bod 而不是 body !
专业提示:如果 bod 不是输入错误,而是api或数据库实际返回的内容,我们总是可以为 body 字段以匹配我们的架构。例如: (parent) => parent.bod 需要记住的一点是,在javascript中,几乎所有东西都是对象。所以如果 post 字段解析为字符串或数字,即 Post 类型仍将尝试在父对象上找到适当命名的属性,不可避免地会失败并返回null。如果一个字段有一个对象类型,但您在其解析程序中返回的不是对象(如字符串或数组),您将不会看到任何类型不匹配的错误,但该字段的子字段将不可避免地解析为null。

常见场景#1: Package 响应

如果我们正在为 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 ,所以我们的解析器应该返回一个具有如下属性的对象 id , title 以及 body . 如果这是我们的api返回的结果,那么我们都准备好了。但是,通常情况下,响应实际上是一个包含额外元数据的对象。因此,我们从端点实际得到的对象可能如下所示:

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

在这种情况下,我们不能只按原样返回响应并期望默认解析器正常工作,因为我们返回的对象没有 id , title 以及 body 我们需要的财产。我们的解析器不需要执行以下操作:

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相反)时,这种 Package 响应也是非常常见的!例如,如果您使用的是node postgres,那么 Result 包含属性的对象,如 rows , fields , rowCount 以及 command . 在将响应返回到解析器之前,需要从该响应中提取适当的数据。

常见场景#2:数组代替对象

如果我们从数据库中获取一个post,解析程序可能如下所示:

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

哪里 Post 是我们通过上下文注入的模型。如果我们使用 sequelize ,我们可以打电话 findAll . mongoose 以及 typeormfind . 这些方法的共同点是,虽然它们允许我们指定 WHERE 条件下,它们返回的承诺仍然解析为数组而不是单个对象。虽然您的数据库中可能只有一个具有特定id的post,但当您调用其中一个方法时,它仍然 Package 在一个数组中。因为数组仍然是一个对象,所以graphql不会解析 post 字段为空。但它会将所有子字段解析为null,因为它无法在数组中找到适当命名的属性。
您只需抓取数组中的第一项并将其返回到解析器中,就可以轻松地修复此情况:

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

如果您从另一个api获取数据,这通常是唯一的选择。另一方面,如果您使用的是orm,那么通常可以使用不同的方法(例如 findOne )它将显式地只从db返回一行(如果不存在则返回null)。

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

关于 INSERT 以及 UPDATE 调用:我们通常期望插入或更新行或模型示例的方法返回插入或更新的行。他们经常这样做,但有些方法不行。例如, sequelizeupsert 方法解析为一个布尔值或上插入的记录的元组和一个布尔值(如果 returning 选项设置为true)。 mongoosefindOneAndUpdate 解析为具有 value 包含已修改行的属性。在将结果返回到解析器之前,请查阅orm的文档并对结果进行适当的解析。

常见场景#3:对象代替数组

在我们的模式中 posts 字段的类型是 ListPost s、 这意味着它的解析器需要返回一个对象数组(或解析为一个对象的承诺)。我们可以这样取回帖子:

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

但是,我们的api的实际响应可能是一个 Package post数组的对象:

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

无法在解析程序中返回此对象,因为graphql需要数组。如果我们这样做,字段将解析为null,并且我们将在响应中看到一个错误,如:
应为iterable,但未找到字段query.posts的iterable。
与上述两种情况不同,在本例中,graphql能够显式检查我们在解析器中返回的值的类型,如果它不是数组那样的iterable,它将抛出。
正如我们在第一个场景中讨论的,为了修复此错误,我们必须将响应转换为适当的形状,例如:

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

不正确使用承诺

js在幕后使用了promise api。因此,解析器可以返回一些值(如 { id: 1, title: 'Hello!' } )或者它可以返回一个承诺,将解决这个价值。对于具有 List 输入时,还可以返回一个承诺数组。如果承诺被拒绝,则该字段将返回null,并且相应的错误将添加到 errors 响应中的数组。如果字段具有对象类型,则promise解析为的值将作为父值传递给任何子字段的解析程序。
promise是“表示异步操作的最终完成(或失败)及其结果值的对象”。下面几个场景概述了在解析程序中处理promise时遇到的一些常见陷阱。但是,如果您不熟悉promises和较新的async/await语法,强烈建议您花一些时间阅读基础知识。
注意:下面的几个例子涉及 getPost 功能。这个函数的实现细节并不重要——它只是一个返回承诺的函数,它将解析为post对象。

常见场景4:不返回值

用于 post 字段可能如下所示:

function post(root, args) {
  return getPost(args.id)
}
``` `getPosts` 回报一个承诺,我们也在回报这个承诺。无论这个承诺决定成为什么,都将成为我们的领域决定的价值。看起来不错!
但如果我们这样做会发生什么:

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

我们仍在创造一个承诺,将解决一个职位。但是,我们并没有回报这个承诺,所以graphql并没有意识到这一点,也不会等待它的解决。在没有显式 `return` 语句隐式返回 `undefined` . 所以我们的函数创建了一个承诺,然后立即返回 `undefined` ,导致graphql为字段返回null。
如果你的承诺 `getPost` 拒绝,我们也不会在响应中看到任何错误——因为我们没有返回承诺,底层代码不关心它是解析还是拒绝。事实上,如果承诺被拒绝,你会看到 `UnhandledPromiseRejectionWarning` 在你的服务器上

相关问题