Перейти к содержанию

Nullability и частичный результат

Mutation Академии уже возвращает связанную запись и курс. Теперь рассмотрим частичный результат, который особенно важен для клиента GraphQL. Ошибка отдельного поля не всегда уничтожает весь ответ. Схема определяет границы null, а исполнение сохраняет соседние данные там, где договор допускает отсутствие значения. Если клиент воспринимает любой errors как полное отсутствие результата, он может скрыть полезные карточки или ошибочно повторить уже принятое изменение.

В состоянии lesson-06 специально меняется вымышленная метаинформация третьего курса: authorId становится a99, которого нет в Map авторов. Сам Course «Доступный интерфейс» сохраняется. Это управляемое учебное условие, не сбой реального внешнего сервиса. Resolver author теперь сообщает GraphQLError, когда связанного автора нет. Благодаря этому пример показывает настоящий путь исполнения поля, а не нарисованный ответ без соответствующих исходников.

Nullable граница связи

Поле Course.author объявлено как Author без восклицательного знака. Когда его resolver бросает ошибку для c03, в результате этого поля появляется null. ID, title и level третьего курса остаются допустимыми. Остальные курсы также сохраняют своих авторов. В errors добавляется запись с сообщением и путём к конкретному проблемному полю. Таким образом, data и errors могут присутствовать одновременно и описывать разные части одного результата.

Ниже показан документ, который раскрывает это условие. Он не использует mutation: мы сначала наблюдаем механизм на чтении, чтобы не смешивать его с вопросом сохранённой Enrollment.

query Authors {
 courses { id title author { id name } }
}

При самостоятельном выполнении ожидается список из трёх курсов, у третьего author:null и ошибка AUTHOR_UNAVAILABLE. Число и порядок карточек не меняются только из-за отсутствия метаданных. Это выбранная граница схемы: курс пригоден для показа без имени автора. Если продукт требует обязательную авторскую ответственность, договор мог бы быть строже, но такой выбор нужно обосновать предметно.

Что меняет non-null

Рассмотрим изменение author на Author!. Теперь null нарушит обещание поля. Исполнение не может оставить его внутри Course и распространит отсутствие значения до ближайшей nullable позиции. Наш список courses имеет тип [Course!]!, поэтому элемент и список также обязательны. В такой комбинации ошибка обязательного author способна привести к data:null на корне. Это не ошибка алгоритма: язык сохраняет объявленное обещание, даже если ради него приходится потерять соседние данные.

Если вместо этого список разрешал nullable элементы, проблемный Course мог бы стать null среди остальных. Если список сам допускает null, граница остановилась бы на нём. Поэтому [Course], [Course!], [Course]! и [Course!]! имеют разные последствия. Полезно читать знаки отдельно для контейнера и элемента, а не воспринимать их как одно общее «обязательное поле». Решение влияет и на клиентские проверки, и на устойчивость частичной выдачи.

Слишком слабая схема заставляет клиент проверять отсутствие там, где предметный договор действительно гарантирует значение. Слишком сильная распространяет отказ дальше, чем нужно интерфейсу. Нельзя выбрать правильную nullability универсальным правилом «ставим восклицательные знаки везде». В Академии ID и title курса обязательны, потому что без них карточка не имеет смысла, а метаданные автора допускают отсутствие. Это конкретное решение небольшого продукта.

Сообщение, код и путь

Для ожидаемой причины используем GraphQLError с extensions.code. Код AUTHOR_UNAVAILABLE удобен клиенту для выбора поведения, а message остаётся коротким человеческим пояснением. Path показывает, где ошибка возникла в форме ответа, включая индекс списка. Если клиент меняет псевдоним поля, путь относится к именам результата, поэтому он помогает сопоставить проблему именно с полученным data, а не только с исходной схемой.

Не вставляем в message внутреннюю SQL-команду, путь файла или полный объект исключения. В текущем Map таких подробностей нет, но правило пригодится при новых источниках. Неожиданная техническая ошибка транспорта получает отдельный безопасный ответ сервера. Ошибка resolver не обязана превращаться в 500 всего HTTP-запроса по нашему учебному договору. Клиент должен прочитать конверт, а не ограничиться response.ok.

Частичный результат после изменения

Если записать студента на c03 и запросить в Enrollment вложенный course.author.name, запись может сохраниться, а метаданные автора отказать. Так как author nullable, основная Enrollment и Course остаются в data. Клиент может показать подтверждение записи и отдельное сообщение о недоступном имени. Полное скрытие результата из-за errors будет неточным представлением фактического состояния операции.

Это не означает, что всегда можно доверять любому частичному ответу как подтверждению изменения. Нужно определить, какое поле является договором результата, где оно nullable и как выполняются побочные эффекты. При отсутствии данных подтверждения нужна процедура восстановления статуса, а не слепое повторение запроса. Наш enroll идемпотентен для пары в одном процессе, однако такое свойство нужно устанавливать предметно для каждой операции, особенно оплаты или отправки уведомления.

Полезный самостоятельный вариант — запросить только id и title, без author. Resolver проблемной связи тогда не нужен, и ожидаемый ответ не содержит её ошибки. Отсутствие ошибки в таком запросе не означает восстановления метаданных: клиент просто не просил это поле. Этот пример показывает, как выбор проекции влияет на наблюдаемые отказы. Структура данных источника остаётся той же, а путь выполнения изменяется документом.

Верните a01 в вымышленную карточку, если хотите пройти обычный вариант, либо оставьте a99 для последующих уроков: архив продолжает сохранять это условие, чтобы финальный клиент умел показать отсутствие автора. Во время подготовки исполнение и проверки не запускались; ожидаемая выдача описана по схеме и функции. Следующая глава добавит контекст текущего студента и отделит право записи от простого присутствия studentId в input.

Path ошибки может содержать индекс списка, который относится именно к текущему ответу. Он не заменяет устойчивый ID курса. После другой сортировки та же карточка окажется на другом месте, поэтому клиент не должен хранить ошибку как постоянное свойство «третьей строки каталога». Для текущего отображения индекс помогает найти участок data, а для длительного сопоставления нужен предметный ключ. Такое разделение предотвращает случайное показанное предупреждение рядом с чужой карточкой после обновления страницы или изменения проекции.

Исходники урока

Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-06. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.

Первичные источники: документация 1.