您的一个或多个字段解析为空有两个常见原因:1)在解析程序中以错误的形状返回数据;2)未正确使用承诺。
注:
如果您看到以下错误:
不能为不可为空的字段返回空值
基础问题是您的字段返回空值。您仍然可以按照下面概述的步骤尝试解决此错误。
以下示例将引用此简单模式:
type Query {
post(id: ID): Post
posts: [Post]
}
type Post {
id: ID
title: String
body: String
}
返回形状错误的数据
我们的模式和请求的查询一起定义了
data
我们的端点返回的响应中的对象。通过形状,我们指的是对象具有什么属性,以及这些属性“值”是否是标量值、其他对象、对象数组或标量。
以同样的方式,模式定义了总响应的形状,
类型
定义字段值的形状。我们在解析器中返回的数据的形状也必须与预期的形状匹配。如果没有,我们的响应中经常会出现意外的空值。
不过,在深入研究特定的示例之前,了解graphql如何解析字段是很重要的。
了解默认冲突解决程序行为
而你当然
可以
为模式中的每个字段编写一个解析器,这通常是不必要的,因为graphql.js在不提供解析器时使用默认解析器。
在较高的层次上,默认的解析器所做的很简单:它查看
起源
字段解析为,如果该值是一个javascript对象,则它将在该对象上使用
同名
当字段被解析时。如果找到该属性,则解析为该属性的值。否则,它解析为空。
比如说在我们的分解器里
post
字段,我们返回值
{ title: 'My First Post', bod: 'Hello World!' }
. 如果我们不为
Post
类型,我们仍然可以请求
邮递
:
query {
post {
id
title
body
}
}
我们的回应是
{
"data": {
"post" {
"id": null,
"title": "My First Post",
"body": null,
}
}
}
这个
title
字段已被解析,尽管我们没有为其提供冲突解决程序,因为默认冲突解决程序执行了繁重的提升——它发现有一个名为
标题
对象上的父字段(在本例中
邮递
)解析为,因此它只解析为该属性的值。这个
id
字段解析为空,因为我们返回的对象
邮递
解析程序没有
身份证件
属性。这个
body
由于输入错误,字段也解析为空--我们有一个名为
bod
而不是
身体
!
专业小费
如果
生化需氧量
是
不
输入错误,但API或数据库实际返回的内容,我们始终可以为
身体
字段以匹配我们的架构。例如:
(parent) => parent.bod
要记住的一件重要的事情是在javascript中,
几乎所有的东西都是一个物体
. 所以如果
邮递
字段解析为字符串或数字,为
柱
类型仍将尝试在父对象上找到一个适当命名的属性,这将不可避免地失败并返回空值。如果一个字段有一个对象类型,但在它的冲突解决程序中返回的不是对象(如字符串或数组),则不会看到关于类型不匹配的任何错误,但该字段的子字段将不可避免地解析为空。
常见场景1:打包响应
如果我们正在为
邮递
查询,我们可以从其他端点获取代码,如:
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
});
}
这个
邮递
字段具有类型
柱
,因此我们的解析器应该返回一个具有如下属性的对象
身份证件
,
标题
和
身体
。如果这就是我们的API返回的结果,那么我们都设置好了。
然而
,响应通常是包含附加元数据的对象。所以我们实际上从端点返回的对象可能如下所示:
{
"status": 200,
"result": {
"id": 1,
"title": "My First Post",
"body": "Hello world!"
},
}
在这种情况下,我们不能只按原样返回响应并期望默认的解析器正常工作,因为我们返回的对象没有
身份证件
,
标题
和
身体
我们需要的属性。我们的解析器不需要做如下的事情:
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
包含以下属性的对象
rows
,
fields
,
rowCount
和
command
. 在将响应返回到解析器之前,需要从该响应中提取适当的数据。
常见场景2:数组而不是对象
如果我们从数据库中获取一个日志,解析程序可能会如下所示:
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
}
在哪里?
柱
是我们通过上下文注入的模型。如果我们使用
sequelize
,我们可以打电话给
findAll
.
mongoose
和
typeorm
有
find
. 这些方法的共同点是,尽管它们允许我们指定
WHERE
条件,他们返回的承诺
仍然解析为数组而不是单个对象
. 虽然您的数据库中可能只有一个具有特定ID的日志,但当您调用其中一个方法时,它仍然被包装在一个数组中。因为数组仍然是一个对象,所以graphql不会解析
邮递
字段为空。但它
将
将所有子字段解析为空,因为它无法在数组中找到适当命名的属性。
只需获取数组中的第一个项并在解析程序中返回该项,就可以轻松地修复此方案:
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
.then(posts => posts[0])
}
如果要从另一个API获取数据,这通常是唯一的选项。另一方面,如果您使用的是ORM,则通常可以使用不同的方法(如
findOne
)这将只显式地从数据库返回一行(如果不存在,则返回空值)。
function post(root, args, context) {
return context.Post.findOne({ where: { id: args.id } })
}
关于
INSERT
和
UPDATE
电话
:我们通常希望插入或更新行或模型实例的方法返回插入或更新的行。他们经常这样做,但有些方法没有。例如,
连续化
的
upsert
方法解析为一个布尔值或一个向上转换的记录和一个布尔值的元组(如果
returning
选项设置为真)。
蒙古斯
的
findOneAndUpdate
解析为具有
value
包含已修改行的属性。在将结果返回到解析器之前,请查阅ORM的文档并适当地分析结果。
常见场景3:对象而不是数组
在我们的模式中,
posts
字段的类型为
List
属于
柱
这意味着它的解析器需要返回一个对象数组(或者一个解析为对象的承诺)。我们可能会得到这样的帖子:
function posts (root, args) {
return fetch('http://SOME_URL/posts')
.then(res => res.json())
}
但是,来自API的实际响应可能是一个包装post数组的对象:
{
"count": 10,
"next": "http://SOME_URL/posts/?page=2",
"previous": null,
"results": [
{
"id": 1,
"title": "My First Post",
"body" "Hello World!"
},
...
]
}
我们不能在解析器中返回这个对象,因为graphql需要一个数组。如果这样做,字段将解析为空,我们将看到响应中包含的错误,如:
应为ITerable,但找不到字段query.posts的ITerable。
与上面的两个场景不同,在本例中,graphql能够显式检查我们在解析器中返回的值的类型,如果它不是
Iterable
就像一个数组。
正如我们在第一个场景中讨论的,为了修复这个错误,我们必须将响应转换为适当的形状,例如:
function posts (root, args) {
return fetch('http://SOME_URL/posts')
.then(res => res.json())
.then(data => data.results)
}
不正确使用承诺
graphql.js利用了引擎盖下的promise api。因此,解析器可以返回一些值(例如
{ id: 1, title: 'Hello!' }
)或者它可以返回一个承诺
决定
到那个值。对于具有
表
类型,您还可以返回一个承诺数组。如果承诺被拒绝,该字段将返回空值,并且相应的错误将添加到
errors
响应中的数组。如果字段具有对象类型,则Promise解析为的值将作为
父值
任何子字段的冲突解决程序。
一
Promise
是一个“对象表示异步操作的最终完成(或失败)及其结果值。”接下来的几个场景概述了在处理冲突解决程序中的承诺时遇到的一些常见陷阱。但是,如果您不熟悉Promises和更新的Async/Await语法,强烈建议您花一些时间阅读基础知识。
音符
:下面的几个示例涉及
getPost
功能。这个函数的实现细节并不重要——它只是一个返回Promise的函数,它将解析为Post对象。
常见场景4:不返回值
工作分解器
邮递
字段可能如下所示:
function post(root, args) {
return getPost(args.id)
}
getPosts
返回承诺,我们将返回该承诺。任何承诺决定的都将成为我们的领域决定的价值。看起来不错!
但是如果我们这样做会发生什么:
function post(root, args) {
getPost(args.id)
}
我们仍在创造一个能解决问题的承诺。但是,我们不会返回承诺,因此graphql不知道它,它不会等待它解决。在没有显式
return
语句隐式返回
undefined
. 所以我们的函数创建了一个承诺,然后立即返回
未定义
,导致graphql为字段返回空值。
如果承诺被
获取日志
拒绝,我们也不会在响应中看到任何错误——因为我们没有返回承诺,基础代码不关心它是解析还是拒绝。事实上,如果承诺被拒绝,你会看到
UnhandledPromiseRejectionWarning
在服务器控制台中。
解决这个问题很简单——只需添加
返回
.
常见场景5:未正确链接承诺
您决定将呼叫结果记录到
获取日志
,所以您将解析器更改为如下所示:
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
})
}
运行查询时,您会看到结果记录在控制台中,但graphql将该字段解析为空。为什么?
当我们打电话的时候
then
就一个承诺而言,我们实际上是在重视承诺的价值,并返回一个新的承诺。你可以这样想
Array.map
除了承诺。
然后
可以返回一个值或另一个承诺。在这两种情况下,返回的内容
然后
被“束缚”在最初的承诺上。多个承诺可以像这样通过使用多个
然后
链中的每一个承诺都是按顺序解决的,最终的价值就是作为最初承诺的价值有效解决的价值。
在上面的示例中,我们没有返回
然后
所以承诺决定
未定义
,其中graphql转换为空值。要解决这个问题,我们必须返回这些帖子:
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
return post // <----
})
}
如果你有多个承诺需要在分解器中解决,你必须使用
然后
并返回正确的值。例如,如果我们需要调用另外两个异步函数(
getFoo
和
getBar
)在我们打电话之前
获取日志
我们可以做到:
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
})
专业提示:
如果您正在努力正确地链接承诺,那么您可能会发现异步/等待语法更清晰,更容易使用。
常见场景6
在承诺之前,处理异步代码的标准方法是使用回调,或者在异步工作完成后调用的函数。例如,我们可能会
蒙古斯
的
芬顿
方法如下:
function post(root, args) {
return Post.findOne({ where: { id: args.id } }, function (err, post) {
return post
})
这里的问题是双重的。第一,回调中返回的值不会用于任何用途(即,它不会以任何方式传递给底层代码)。第二,当我们使用回调时,
Post.findOne
不返回承诺;只是返回未定义的。在本例中,将调用回调,如果记录
邮递
我们将看到从数据库返回的内容。但是,由于我们没有使用promise,graphql不会等待这个回调完成——它接受返回值(未定义)并使用它。
最受欢迎的图书馆,包括
蒙古斯
现成的支持承诺。那些不经常有免费的“包装器”库来添加这个功能的用户。
在使用graphql解析器时,应该避免使用使用回调的方法,而是使用返回承诺的方法。
专业提示:
同时支持回调和承诺的库经常以这样的方式重载其函数:如果不提供回调,函数将返回承诺。有关详细信息,请查阅图书馆的文档。
如果您必须使用回调,也可以将回调包装在承诺中:
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)
}
})
})