gpt4 book ai didi

graphql - 为什么GraphQL查询返回null?

转载 作者:行者123 更新时间:2023-11-29 15:55:46 30 4
gpt4 key购买 nike

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

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

如果我使该字段为非空,则在响应中的errors数组内会看到以下错误:


  无法为不可为空的字段返回null


为什么GraphQL不返回数据?

最佳答案

您的一个或多个字段解析为null的常见原因有两个:1)返回解析器内部形状错误的数据;和2)没有正确使用Promises。

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


  无法为不可为空的字段返回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
}
}


我们的回应是

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


即使我们没有为其提供解析器,也已解析了 title字段,因为默认解析器完成了繁重的工作-它看到在父字段的Object字段上有一个名为 title的属性(在本例中为 )解析为,因此它只是解析为该属性的值。 post字段解析为null,因为我们在 id解析器中返回的对象没有 post属性。由于输入错误, id字段也解析为null -我们有一个名为 body的属性,而不是 bod


  专家提示:如果 body不是拼写错误,而是API或数据库实际返回的内容,我们总是可以为 bod字段编写一个解析器以匹配我们的模式。例如: body


要记住的重要一件事是,在JavaScript中,几乎所有东西都是对象。因此,如果 (parent) => parent.bod字段解析为字符串或数字,则 post类型上的每个字段的默认解析器仍将尝试在父对象上查找适当命名的属性,不可避免地失败并返回null。如果字段具有对象类型,但在其解析程序中返回的对象不是对象(例如String或Array),则不会看到有关类型不匹配的任何错误,但该字段的子字段将不可避免地解析为null。

常见场景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,因此我们的解析器应返回具有 Postidtitle之类的属性的对象。如果这是我们的API返回的结果,那么我们都准备就绪。但是,通常将响应实际上是包含其他元数据的对象。因此,我们实际上从端点返回的对象可能看起来像这样:

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


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

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,则会得到一个 body对象,该对象包含 ResultrowsfieldsrowCount之类的属性。您需要先从此响应中提取适当的数据,然后再将其返回到解析器中。

常见方案#2:数组而不是对象

如果我们从数据库中获取帖子,解析器可能看起来像这样:

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


其中 command是我们通过上下文注入的某种模型。如果使用的是 Post,则可以调用 sequelizefindAllmongoose具有 typeorm。这些方法的共同之处在于,尽管它们允许我们指定 find条件,但它们返回的Promise仍然解析为数组而不是单个对象。虽然数据库中可能只有一个帖子具有特定ID,但是当您调用这些方法之一时,它仍包裹在数组中。因为数组仍然是对象,所以GraphQL不会将 WHERE字段解析为null。但这会将所有子字段解析为null,因为它将无法在数组上找到适当命名的属性。

您只需抓住数组中的第一项并将其返回到解析器中即可轻松解决此情况:

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


如果要从另一个API提取数据,则通常是唯一的选择。另一方面,如果您使用的是ORM,通常可以使用其他方法(例如 post),该方法将显式地仅从数据库返回一行(如果不存在,则返回null)。

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


关于 findOneINSERT调用的特别说明:我们通常希望插入或更新行或模型实例的方法返回插入或更新的行。他们经常这样做,但是有些方法却没有。例如, UPDATEsequelize方法解析为布尔值,或者是加高后的记录和布尔值的元组(如果 upsert选项设置为true)。 returningmongoose解析为具有 findOneAndUpdate属性的对象,其中包含修改后的行。在将结果返回到解析器之前,请查阅您的ORM文档并适当地分析结果。

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

在我们的模式中, value字段的类型是 postsList,这意味着其解析器需要返回对象数组(或解析为一个的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,我们将在响应中看到一个错误,例如:


  预期为Iterable,但未为字段Query.posts找到一个。


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

就像我们在第一种情况中讨论的那样,为了解决此错误,我们必须将响应转换为适当的形状,例如:

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


没有正确使用承诺

GraphQL.js在后台使用Promise API。这样,解析程序可以返回某个值(例如 Post),也可以返回将解析为该值的Promise。对于具有 { id: 1, title: 'Hello!' }类型的字段,您还可以返回一个Promises数组。如果Promise拒绝,则该字段将返回null,并且相应的错误将添加到响应中的 List数组。如果字段具有对象类型,则Promise解析为的值将作为父值向下传递给任何子字段的解析器。

Promise是“对象表示异步操作的最终完成(或失败)及其结果值”。接下来的几种情况概述了在解析器内部处理Promises时遇到的一些常见陷阱。但是,如果您对Promises和较新的async / await语法不熟悉,强烈建议您花一些时间来阅读基础知识。

注意:以下几个示例引用了 errors函数。该函数的实现细节并不重要-只是返回Promise的函数,该函数将解析为post对象。

常见方案4:不返回值

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

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


post返回一个Promise,我们返回该Promise。无论Promise决定解决什么,它将成为我们领域解决的价值。看起来不错!

但是如果执行此操作会发生什么:

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


我们仍在创建一个可解决帖子的Promise。但是,我们没有返回Promise,因此GraphQL并不知道它,也不会等待它解决。在没有显式 getPosts语句的JavaScript函数中,将隐式返回 return。因此,我们的函数创建一个Promise,然后立即返回 undefined,使GraphQL为该字段返回null。

如果 undefined返回的Promise拒绝,我们也不会在响应中列出任何错误-因为我们没有返回Promise,所以底层代码并不关心它是解决还是拒绝。实际上,如果Promise拒绝,您将在服务器控制台中看到 getPost

解决此问题很简单-只需添加
UnhandledPromiseRejectionWarning


常见方案5:未正确链接承诺

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

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


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

当我们在Promise上调用 getPost时,我们实际上是在使用Promise解析的值并返回一个新的Promise。您可以想到它类似于 then,但Promises除外。 Array.map可以返回一个值或另一个Promise。无论哪种情况, then内部返回的内容都“链接”到原始Promise上。可以通过使用多个 then将多个Promise链接在一起。链中的每个Promise都是按顺序解决的,最终值就是有效地解析为原始Promise的值。

在上面的示例中,我们在 then内未返回任何内容,因此Promise解析为 then,GraphQL将其转换为null。要解决此问题,我们必须退回帖子:

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


如果您有多个Promises,则需要在解析器内部进行解析,则必须使用 undefined并返回正确的值来正确地链接它们。例如,如果需要在调用 then之前调用其他两个异步函数( getFoogetBar),则可以执行以下操作:

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
})



  专家提示:如果您在正确地链接Promises方面遇到困难,则可能会发现async / await语法更简洁,更易于使用。


常见场景#6

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

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


这里的问题有两个。第一,在回调函数中返回的值不用于任何事情(即,它不会以任何方式传递给基础代码)。第二,当我们使用回调时, findOne不会返回Promise。它只是返回未定义。在此示例中,我们的回调将被调用,如果我们记录 Post.findOne的值,我们将看到从数据库返回的任何内容。但是,因为我们没有使用Promise,所以GraphQL不会等待此回调完成-它使用返回值(未定义)并使用它。

最受欢迎的库,包括 post支持开箱即用。那些不经常使用免费的“包装”库来添加此功能的库。使用GraphQL解析器时,应避免使用利用回调的方法,而应使用返回Promises的方法。


  专家提示:同时支持回调和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)
}
})
})

关于graphql - 为什么GraphQL查询返回null?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56468150/

30 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com