处理可空性和错误
让您的查询更加类型安全
非空性注释目前在于 Apollo Kotlin 中 experimental 中。如果您对它们有反馈,请通过 GitHub issues, 在 Kotlin Slack 社区, 或者 GraphQL 非空性工作小组 来告诉我们。
介绍
本节是对 GraphQL 中的非空性及其错误的高级描述,这些错误对应用开发者造成的问题。关于提出的解决方案,请直接跳转到 @semanticNonNull 部分。
GraphQL 没有内置 Result
类型。如果一个 字段 出现错误,它将在 JSON 响应中设置为 null
,并将错误添加到 errors
数组中。
仅从 Schema 中,无法确定 null
是否为一个有效的业务值或仅针对错误发生:
type User {id: ID!# Should the UI deal with user without a name here? It's impossible to tell.name: StringavatarUrl: String}
GraphQL的最佳实践建议将字段默认设置为可空,以处理错误。
来自graphql.org:
在GraphQL类型系统中,每个字段默认都是可空的。这是因为许多事情可能会在由数据库和其他服务支持的网络服务中出现错误。
例如,以下查询在出现错误时得到如下响应:
query GetUser {user {idnameavatarUrl}}
...
{"data": {"user": {"id": "1001","name": null,"avatarUrl": "https://example.com/pic.png"}},"errors": [{"message": "Cannot resolve user.name","path": ["user", "name"]}]}
对于前端开发者来说,这种可空的默认值有一个主要的缺点。它要求你仔细检查UI代码中的每个 字段。
有时并不清楚如何处理不同的案例
@Composablefun User(user: GetUserQuery.User) {if (user.name != null) {Text(text = user.name)} else {// What to do here?// Is it an error?// Is it a true null?// Should I display a placeholder? an error? hide the view?}}
当有很多 字段 时,手动处理每个 null
情况变得非常繁琐。
如果UI能够决定更全局地处理错误,并在任何 字段 失败时显示一个通用错误,那岂不是很好?
Apollo Kotlin 提供了nullability 指令 来处理这种情况:
这些工具将GraphQL的默认配置从“处理每个 字段 的错误”更改为“选择您希望处理的错误”。
导入nullability指令
Nullability 指令 是实验性的。您需要使用 @link
指令:
extend schema @link(url: "https://specs.apollo.dev/nullability/v0.4",# Note: other directives are needed later on and added here for convenienceimport: ["@semanticNonNull", "@semanticNonNullField", "@catch", "CatchTo", "@catchByDefault"])
ⓘ 注意
您还需要选择默认的捕获,但更多内容将在稍后介绍 。。
@semanticNonNull
@semanticNonNull
在GraphQL的 类型系统 中引入了一个新类型。
A @semanticNonNull
类型永远不会为 null,除非在 errors 数组中存在错误。
在您的模式中使用它
type User {id: ID!# name is never null unless there is an errorname: String @semanticNonNull# avatarUrl may be null even if there is no error. In that case the UI should be prepared to display a placeholder.avatarUrl: String}
ⓘ 注意
@semanticNonNull
是一个 指令,因此可以在不破坏当前 GraphQL 工具的情况下引入,但最终目标是引入新的语法。有关详细信息,请参阅 nullability 工作组讨论。
对于 fields 的 List
类型,@semanticNonNull
仅应用于第一级。如果您需要将其应用于给定的级别,请使用 levels
参数:
type User {# adminRoles may be null if the user is not an admin# if the user is an admin, adminRoles[i] is never null unless there is also an erroradminRoles: [AdminRole] @semanticNonNull(levels: [1])}
使用 @semanticNonNull
,前端开发人员知道给定的 field 在常规 操作 中永远不会为 null,因此可以根据此采取相应措施。无需再猜测了!
理想情况下,您的 backend 团队使用 @semanticNonNull
指令为其模式进行注释,以便不同的前端团队能够从新的类型信息中受益。
有时这个过程需要时间。
对于这些情况,您可以通过在额外的 graphqls 文件中使用 @semanticNonNullField
来扩展您的模式:
# Same effect as above but works as a schema extensionsextend type User @semanticNonNullField(name: "name")
您稍后可以与您的 backend 团队分享该文件,并确保您对类型的解释是正确的。
@catch
虽然 @semanticNonNull
是一个描述您数据的 server directive,但 @catch
是一个定义如何处理错误的 client directive。
@catch
允许以下操作:
- 将错误处理为
FieldResult<T>
,获取访问相邻错误的权限。 - 抛出错误并让其他父 field处理它或冒泡到
data == null
。 - 将错误强制转换为
null
(当前 GraphQL 默认设置)。
对于 fields 的 List
类型,@catch
仅应用于第一级。如果您需要将其应用于给定的级别,请使用 levels
参数:
query GetUser {user {# map friends[i] to FieldResultfriends @catch(to: RESULT, level: 1)}}
放置错误
要获取放置错误,请使用 @catch(to: RESULT)
:
query GetUser {user {id# map name to FieldResult<String> instead of stopping parsingname @catch(to: RESULT)}}
上述查询生成以下 Kotlin 代码:
class User(val id: String,// note how String is not nullable. This is because name// was marked `@semanticNonNull` in the previous section.val name: FieldResult<String>,)
使用 getOrNull()
获取值:
println(user.name.getOrNull()) // "Luke Skywalker"// or you can also decide to throw on errorprintln(user.name.getOrThrow())
使用 graphQLErrorOrNull()
获取放置的错误:
println(user.name.graphQLErrorOrNull()) // "Cannot resolve user.name"
抛出错误
要抛出错误,请使用 @catch(to: THROW)
:
query GetUser {user {id# throw any errorname @catch(to: THROW)}}
上述查询生成以下 Kotlin 代码:
class User(val id: String,val name: String,)
ⓘ 注意
错误在解析过程中抛出,但在到达您的UI代码之前被捕获。如果没有父字段捕获它,Apollo Kotlin运行时会捕获并把它设置为 ApolloResponse.exception
。
将错误强制转换为null
要将错误强制转换为 null
(当前GraphQL默认值),请使用 @catch(to: NULL)
:
query GetUser {user {id# coerce errors to nullname @catch(to: NULL)}}
上述查询生成以下 Kotlin 代码:
class User(val id: String,// Note how name is nullable again despite being marked// @semanticNonNull in the schemaval name: String?,)
ⓘ 注意
错误在解析过程中抛出,但在到达您的UI代码之前被捕获。如果没有父字段捕获它,Apollo Kotlin运行时会做并将在 ApolloResponse.exception
中暴露异常。
@catchByDefault
要使用可空性指令,您需要为可空的GraphQL字段选择默认捕获行为,使用 @catchByDefault
。
您可以选择将可空字段映射到 FieldResult
:
# Errors stop the parsing.extend schema @catchByDefault(to: RESULT)
或抛出错误
# Errors stop the parsing.extend schema @catchByDefault(to: THROW)
或将错误强制转换为 null
,如当前GraphQL默认:
# Coerce errors to null by default.extend schema @catchByDefault(to: NULL)
(添加 @catchByDefault(to: NULL)
对代码生成器不起作用,但可以使用 @catch
在您的操作 中。)
迁移到语义可空性
语义可空性对默认可空的模式最有用。这些都是需要“处理每个字段错误”的模式。
为了将此默认值更改为“选择您想要处理的错误”,您可以使用以下方法
- 导入可空性指令。
- 默认为转换为null:
extend schema @catchByDefault(to: NULL)
. 这是一个无操作,可以开始探索指令。 - 为单个字段添加
@catch
,更熟悉它的工作方式。 - 当准备好进行大切换时,将
extend schema catch(to: THROW)
更改为(同时)在所有操作/ 片段 上添加query GetFoo @catch(to: NULL) {}
(这是一个无操作)。 - 从现在开始,新查询默认使用
catch(to: THROW)
。 - 逐步删除
query GetFoo @catch(to: NULL) {}
。