Есть две распространенные причины, по которым ваше поле или поля разрешаются в null: 1) возврат данных в неправильной форме внутри вашего преобразователя; и 2) неправильное использование Promises.
Примечание: если вы видите следующую ошибку:
Невозможно вернуть значение NULL для поля, не допускающего значение NULL
основная проблема заключается в том, что ваше поле возвращает значение null. Вы все еще можете выполнить шаги, описанные ниже, чтобы попытаться устранить эту ошибку.
Следующие примеры относятся к этой простой схеме:
type Query {
post(id: ID): Post
posts: [Post]
}
type Post {
id: ID
title: String
body: String
}
Возврат данных в неправильной форме
Наша схема вместе с запрошенным запросом определяет "форму" объекта data
в ответе, возвращаемом нашей конечной точкой. Под формой мы подразумеваем, какие свойства имеют объекты, и являются ли значения этих свойств скалярными значениями, другими объектами или массивами объектов или скаляров.
Точно так же, как схема определяет форму общего ответа, тип отдельного поля определяет форму значения этого поля. Форма данных, которые мы возвращаем в наш распознаватель, также должна соответствовать этой ожидаемой форме. Когда это не так, мы часто получаем неожиданные пустые значения в нашем ответе.
Прежде чем мы углубимся в конкретные примеры, важно понять, как GraphQL разрешает поля.
Понимание поведения преобразователя по умолчанию
Хотя вы, безусловно, можете написать преобразователь для каждого поля в вашей схеме, часто в этом нет необходимости, поскольку GraphQL.js использует преобразователь по умолчанию, когда вы его не предоставляете.
На высоком уровне, то, что делает преобразователь по умолчанию, очень просто: он просматривает значение поля parent, разрешенное в поле, и если это значение является объектом 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
было разрешено, несмотря на то, что мы не предоставили для него преобразователь, потому что преобразователь по умолчанию сделал тяжелую работу — он увидел свойство с именем title
в родительском поле объекта (в этом case post
) разрешается в и поэтому просто разрешается в значение этого свойства. Поле id
разрешено как null, потому что объект, который мы вернули в нашем преобразователе post
, не имел свойства id
. Поле body
также разрешено как null из-за опечатки — у нас есть свойство с именем bod
вместо body
!
Совет: если bod
является не опечаткой, а тем, что на самом деле возвращает API или база данных, мы всегда можем написать сопоставитель для нашего поля <5727570>497 схема. Например: (parent) => parent.bod
Важно помнить, что в JavaScript почти все является объектом. Таким образом, если поле post
разрешается в строку или число, сопоставитель по умолчанию для каждого из полей типа 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
, поэтому наш преобразователь должен возвращать объект со свойствами, такими как 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)! Например, если вы используете node-postgres, вы получите объект Result
, который включает такие свойства, как rows
, fields
, rowCount
и <572758750>. Вам нужно будет извлечь соответствующие данные из этого ответа, прежде чем возвращать его в ваш преобразователь.
Общий сценарий #2: Массив вместо объекта
Что, если мы получим сообщение из базы данных, наш преобразователь может выглядеть примерно так:
function post(root, args, context) {
return context.Post.find({ where: { id: args.id } })
}
где Post
— некоторая модель, которую мы внедряем через контекст. Если мы используем sequelize
, мы можем вызвать findAll
. mongoose
и typeorm
имеют find
. Что общего у этих методов, так это то, что, хотя они позволяют нам указать условие WHERE
, возвращаемые ими промисы по-прежнему разрешаются в массив вместо одного объекта. Хотя в вашей базе данных, вероятно, есть только одна запись с определенным идентификатором, она все равно заключена в массив, когда вы вызываете один из этих методов. Поскольку массив по-прежнему является объектом, GraphQL не будет разрешать поле post
как нулевое. Но он разрешит все дочерние поля как нулевые, поскольку не сможет найти свойства с соответствующими именами в массиве.
Вы можете легко исправить этот сценарий, просто захватив первый элемент в массиве и вернув его в свой преобразователь:
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
имеет тип List
из Post
, что означает, что его сопоставитель должен возвращать массив объектов (или обещание, которое разрешается в единицу). Мы могли бы получить такие сообщения:
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 внутри использует API Promise. Таким образом, преобразователь может возвращать некоторое значение (например, { id: 1, title: 'Hello!' }
) или может возвращать обещание, которое будет resolve для этого значения. Для полей типа List
вы также можете вернуть массив промисов. Если обещание отклонено, это поле вернет значение null, и соответствующая ошибка будет добавлена в массив errors
в ответе. Если поле имеет тип Object, то значение, в которое преобразуется Promise, будет передано как родительское значение преобразователям любых дочерних полей.
A Promise — это «объект, представляющий возможное завершение (или сбой) асинхронной операции и его результирующее значение». Следующие несколько сценариев описывают некоторые распространенные ловушки, возникающие при работе с промисами внутри преобразователей. Однако, если вы не знакомы с Promises и новым синтаксисом async/await, настоятельно рекомендуется потратить некоторое время на изучение основ.
Примечание: следующие несколько примеров относятся к функции getPost
. Детали реализации этой функции не важны — это просто функция, которая возвращает обещание, которое будет преобразовано в объект сообщения.
Распространенный сценарий №4: не возвращается значение
Рабочий преобразователь для поля post
может выглядеть следующим образом:
function post(root, args) {
return getPost(args.id)
}
getPosts
возвращает обещание, и мы возвращаем это обещание. Что бы ни разрешало это обещание, оно станет значением, которое разрешает наше поле. Хорошо выглядишь!
Но что произойдет, если мы сделаем это:
function post(root, args) {
getPost(args.id)
}
Мы все еще создаем обещание, которое будет преобразовано в публикацию. Однако мы не возвращаем обещание, поэтому GraphQL не знает об этом и не будет ждать его разрешения. В JavaScript функции без явного оператора return
неявно возвращают undefined
. Итак, наша функция создает промис, а затем немедленно возвращает undefined
, в результате чего GraphQL возвращает значение null для поля.
Если промис, возвращенный getPost
, отклоняется, мы также не увидим никаких ошибок в нашем ответе — поскольку мы не вернули промис, базовый код не заботится о том, разрешается он или отклоняется. На самом деле, если обещание отклонено, вы увидите
UnhandledPromiseRejectionWarning
в консоли сервера.
Решить эту проблему просто — просто добавьте return
.
Распространенный сценарий №5: неправильное связывание промисов в цепочку
Вы решили зарегистрировать результат вашего звонка на getPost
, поэтому вы изменили свой преобразователь, чтобы он выглядел примерно так:
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
})
}
Когда вы запускаете свой запрос, вы видите результат, зарегистрированный в вашей консоли, но GraphQL разрешает поле как пустое. Почему?
Когда мы вызываем then
для промиса, мы фактически берем значение, на которое промис разрешен, и возвращаем новый промис. Вы можете думать об этом как о Array.map
, за исключением промисов. then
может возвращать значение или другое обещание. В любом случае то, что возвращается внутри then
, "привязано" к исходному промису. Несколько промисов можно связать вместе, используя несколько then
. Каждое обещание в цепочке разрешается последовательно, и окончательное значение — это то, что эффективно разрешается как значение исходного обещания.
В нашем примере выше мы ничего не вернули внутри then
, поэтому промис разрешился в undefined
, который GraphQL преобразовал в нуль. Чтобы исправить это, мы должны вернуть сообщения:
.
function post(root, args) {
return getPost(args.id)
.then(post => {
console.log(post)
return post // <----
})
}
Если у вас есть несколько обещаний, которые вам нужно разрешить внутри вашего преобразователя, вы должны правильно связать их в цепочку, используя then
и возвращая правильное значение. Например, если нам нужно вызвать две другие асинхронные функции (getFoo
и getBar
), прежде чем мы сможем вызвать 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
})
Совет: Если вы боретесь с правильной цепочкой промисов, вы можете обнаружить, что синтаксис async/await чище и проще в работе.
Общий сценарий #6
До промисов стандартным способом обработки асинхронного кода было использование обратных вызовов или функций, которые вызывались после завершения асинхронной работы. Мы могли бы, например, вызвать метод mongoose
findOne
следующим образом:
function post(root, args) {
return Post.findOne({ where: { id: args.id } }, function (err, post) {
return post
})
Проблема здесь двоякая. Во-первых, значение, возвращаемое внутри обратного вызова, ни для чего не используется (т. е. оно никоим образом не передается базовому коду). Во-вторых, когда мы используем обратный вызов, Post.findOne
не возвращает обещание; он просто возвращает undefined. В этом примере будет вызван наш обратный вызов, и если мы зарегистрируем значение post
, мы увидим, что было возвращено из базы данных. Однако, поскольку мы не использовали промис, GraphQL не ждет завершения этого обратного вызова — он принимает возвращаемое значение (неопределенное) и использует его.
Большинство популярных библиотек, в том числе mongoose
, изначально поддерживают обещания. Те, которые не часто имеют бесплатные библиотеки-оболочки, которые добавляют эту функциональность. При работе с преобразователями 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)
}
})
})
Примечание. Этот вопрос предназначен для использования в качестве справочного вопроса и потенциального обмана для подобных вопросов. Вот почему вопрос является широким и опускает какие-либо конкретные детали кода или схемы. Дополнительные сведения см. в этом метасообщении.
Я думаю, вам следует изменить заголовок, так как его по-прежнему нелегко найти по «Невозможно вернуть значение null для поля, допускающего значение NULL» или даже «[graphql] Невозможно вернуть значение null для поля, допускающего значение NULL». поле, допускающее значение null - почему оно возвращает значение null?" ?