你的字段或几个字段返回null的原因通常有两个:1)在解析器内部以错误的形式返回数据;2)未正确使用Promises。
注意:如果您看到以下错误:
Cannot return null for non-nullable field
根本问题是您的字段返回null。 您仍然可以按照下面概述的步骤尝试解决此错误。
以下示例将引用此简单模式:
type Query {
post(id: ID): Post
posts: [Post]
}
type Post {
id: ID
title: String
body: String
}
返回错误的数据格式
我们的schema和请求查询一起定义了我们在端点响应中返回的"data"对象的“形状”。 形状指对象具有的属性以及这些属性值是标量值,其他对象还是对象或标量的数组。
就像模式定义总响应的形状一样,单个字段的类型也定义了该字段值的形状。 我们在解析器中返回的数据形状必须与此预期的形状相匹配。 当不匹配时,我们经常会在响应中得到意外的null。
然而,在我们深入具体示例之前,重要的是要理解GraphQL如何解析字段。
了解默认解析器行为
虽然您确实可以为模式中的每个字段编写解析器,但通常不必这样做,因为当您未提供解析器时,GraphQL.js将使用默认解析器。
从高层次上讲,默认解析器的工作原理很简单:它查看父字段解析到的值,如果该值是JavaScript对象,则查找该对象上具有相同名称的属性名的属性。 如果找到该属性,则解析为该属性的值。否则,将解析为null。
假设在我们的post
字段的解析器中,我们返回值{ 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
字段被解析了,即使我们没有为它提供解析器,因为默认的解析器已经做了重活——它看到了父字段(在本例中为
post
)解析到的对象上有一个名为
title
的属性,所以它只是解析了那个属性的值。
id
字段解析为null,因为我们在
post
解析器中返回的对象没有
id
属性。由于拼写错误,
body
字段也解析为null——我们有一个名为
bod
而不是
body
的属性!
专业提示:如果bod
不是拼写错误而是API或数据库实际返回的内容,我们可以始终为body
字段编写解析器以匹配我们的模式。例如:(parent) => parent.bod
需要记住的一件重要事情是,在JavaScript中,几乎所有东西都是对象。因此,如果post
字段解析为字符串或数字,则Post
类型的每个字段的默认解析器仍会尝试在父对象上找到适当命名的属性,但不可避免地失败并返回null。如果字段具有对象类型,但在其解析器中返回其他类型(如字符串或数组),则您将不会看到有关类型不匹配的任何错误,但该字段的子字段将不可避免地解析为null。
常见场景#1:包装响应
如果我们正在编写post
查询的解析器,我们可能会从一些其他端点获取代码,像这样:
function post (root, args) {
return axios.get(`http://SOME_URL/posts/${args.id}`)
.then(res => res.data);
return fetch(`http://SOME_URL/posts/${args.id}`)
.then(res => res.json());
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) {
return axios.get(`http://SOME_URL/posts/${args.id}`)
.then(res => res.data.result);
return fetch(`http://SOME_URL/posts/${args.id}`)
.then(res => res.json())
.then(data => data.result);
return request({
uri: `http://SOME_URL/posts/${args.id}`,
json: true
})
.then(res => res.result);
}
注意:上面的示例从另一个端点提取数据;然而,当直接使用数据库驱动程序(而不是使用ORM)时,这种包装的响应也非常常见!例如,如果您使用node-postgres,您将获得一个Result
对象,该对象包括rows
、fields
、rowCount
和command
等属性。您需要从此响应中提取适当的数据,然后将其返回到解析器中。
常见情况 #2:数组而不是对象
如果我们从数据库中获取帖子,我们的解析器可能会像这样:
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
}
这里的Post
是我们通过上下文注入的某个模型。如果我们使用的是sequelize
,我们可能会调用findAll
;mongoose
和typeorm
则有find
方法。这些方法的共同之处在于,虽然它们允许我们指定WHERE
条件,但它们返回的Promises仍然解析成数组而不是单个对象。虽然您的数据库中可能只有一个特定ID的帖子,但在调用这些方法时它仍然被包装在一个数组中。由于数组仍然是一个对象,因此GraphQL不会将post
字段解析为null。但它 会将所有子字段解析为null,因为它找不到数组上的适当命名的属性。
您可以很容易地通过只获取数组中的第一个项目并将其返回给您的解析器来解决这种情况:
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
.then(posts => posts[0])
}
如果你从另一个API中获取数据,这通常是唯一的选项。另一方面,如果你使用ORM,则通常有不同的方法可用(比如findOne
),它将明确地仅返回数据库中的单个行(或null,如果不存在)。
function post(root, args, context) {
return context.Post.findOne({ where: { id: args.id } })
}
关于INSERT
和UPDATE
调用的特别说明:我们经常期望插入或更新行或模型实例的方法返回已插入或已更新的行。通常他们确实会这样做,但有些方法不会。例如,
sequelize
的
upsert
方法解析为一个布尔值,或者一个元组(如果将
returning
选项设置为true,则包含已插入的记录和一个布尔值)。
mongoose
的
findOneAndUpdate
解析为一个带有
value
属性的对象,该属性包含修改后的行。在返回解析结果之前,请查阅你所使用的ORM文档并适当地解析结果。
常见场景#3:对象而不是数组
在我们的模式中,posts
字段的类型是Post
列表,这意味着它的解析器需要返回一个对象数组(或解析为该数组的Promise)。我们可以像这样获取帖子:
function posts (root, args) {
return fetch('http://SOME_URL/posts')
.then(res => res.json())
}
然而,我们的API实际返回的响应可能是一个包装文章数组的对象:
{
"count": 10,
"next": "http://SOME_URL/posts/?page=2",
"previous": null,
"results": [
{
"id": 1,
"title": "My First Post",
"body" "Hello World!"
},
...
]
}
我们不能在解析器中返回该对象,因为GraphQL需要一个数组。如果我们这样做,该字段将解析为null,并且我们将在响应中看到一个错误,如下所示:
期望迭代对象,但未在Query.posts字段中找到。
与上面两种情况不同,在这种情况下,GraphQL能够显式检查我们在解析器中返回的值的类型,如果它不是像数组一样的可迭代对象,则会引发异常。
就像我们在第一个场景中讨论的那样,为了修复此错误,我们必须将响应转换为适当的格式,例如:
function posts (root, args) {
return fetch('http://SOME_URL/posts')
.then(res => res.json())
.then(data => data.results)
}
不正确地使用 Promises
GraphQL.js 在内部使用 Promise API。因此,一个解析器可以返回一些值(比如 { id: 1, title: 'Hello!' }
),或者返回一个将会被resolve为该值的 Promise。对于具有 List
类型的字段,您还可以返回一个 Promise 数组。如果 Promise 被拒绝,该字段将返回 null,并且相应的错误将被添加到响应中的 errors
数组中。如果一个字段具有对象类型,则 Promise 解析为的值将作为该字段的父值传递到任何子字段的解析器。
Promise 是一个“表示异步操作的最终完成(或失败)及其结果值的对象。” 接下来的几种情况概述了在解析器中处理 Promises 时遇到的一些常见问题。然而,如果您不熟悉 Promises 和较新的 async/await 语法,强烈建议您花些时间学习基础知识。
注意:下面的示例都是针对一个 getPost
函数。这个函数的实现细节并不重要--它只是一个返回 Promise 的函数,这个 Promise 将解析为一个帖子对象。
常见情况#4:未返回值
post
字段的工作解析器可能是这样的:
function post(root, args) {
return getPost(args.id)
}
getPosts
返回一个 Promise,我们正在返回该 Promise。无论该 Promise 解决为什么值,它都将成为我们字段解决为的值。看起来很好!
但是如果我们这样做会发生什么:
function post(root, args) {
getPost(args.id)
}
我们仍在创建一个将解析为帖子的Promise。但是,我们没有返回Promise,因此GraphQL不知道它,并且不会等待它的解决。在JavaScript中,如果函数没有显式的
return
语句,则会隐式返回
undefined
。因此,我们的函数创建了一个Promise,然后立即返回
undefined
,导致GraphQL返回null。
如果
getPost
返回的Promise被拒绝,我们也不会在响应中看到任何错误--因为我们没有返回Promise,底层代码不关心它是解决还是拒绝。实际上,如果Promise被拒绝,您会在服务器控制台上看到
UnhandledPromiseRejectionWarning
错误。
修复这个问题很简单--只需要添加
return
即可。
常见情况#5:未正确链接Promise
您决定记录对
getPost
的调用结果,因此您将解析器更改为类似以下内容的样子:
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
})
}
运行查询时,查询结果会在控制台中记录,但GraphQL会将字段解析为空值。为什么?
当我们在Promise上调用then
时,实际上是获取Promise解析的值并返回一个新的Promise。你可以把它想象成类似于Array.map,只不过是针对Promises的。 then
可以返回一个值或另一个Promise。无论哪种情况,在then
内部返回的内容都会被“链接”到原始Promise上。通过使用多个then
,可以像这样将多个Promise链接在一起。链中的每个Promise按顺序解决,并且最终值有效地作为原始Promise的值得到解决。
在我们上面的示例中,我们在then
内部没有返回任何内容,因此Promise解析为undefined
,GraphQL将其转换为null。要解决这个问题,我们必须返回帖子:
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
return post
})
}
如果你需要在resolver中解决多个Promise,你需要使用then
正确地链式调用它们并返回正确的值。例如,如果我们需要在调用getPost
之前先调用另外两个异步函数(getFoo
和getBar
),我们可以这样做:
function post(root, args) {
return getFoo()
.then(foo => {
return getBar()
})
.then(bar => {
return getPost(args.id)
})
专业提示:如果您在正确链接Promises方面遇到困难,您可能会发现使用async/await语法更加清晰易用。
常见情况#6
在Promises出现之前,处理异步代码的标准方式是使用回调函数,或者在异步工作完成后调用的函数。例如,我们可以这样调用mongoose
的findOne
方法:
function post(root, args) {
return Post.findOne({ where: { id: args.id } }, function (err, post) {
return post
})
这里的问题是双重的。首先,在回调函数内返回的值没有被用于任何事情(即它没有以任何方式传递给底层代码)。其次,当我们使用回调函数时,Post.findOne
不会返回一个 Promise;它只返回 undefined。在这个例子中,我们的回调函数将被调用,如果我们记录 post
的值,我们将看到从数据库返回的任何内容。但是,因为我们没有使用 Promise,GraphQL 不会等待这个回调函数完成——它获取返回值(undefined)并使用它。
大多数流行的库,包括 mongoose
,都支持 Promises。那些不支持的库通常有补充的“包装”库添加此功能。在处理 GraphQL 解析器时,应避免使用使用回调的方法,而是使用返回 Promise 的方法。
专业提示:支持回调和 Promises 的库通常会过载其函数,以便如果未提供回调,则函数将返回 Promise。请查阅该库的文档以了解详情。
如果您绝对必须使用回调函数,也可以将回调函数包装在 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)
}
})
})