我有一个 GraphQL 项目,其中许多突变返回一个简单的布尔值以表示成功(与错误类型联合)。
# An object wrapper for a un/successful mutation result
type MutationResult {
# whether the mutation was successful
success: Boolean!
}
# An indicator that a user-error
type FieldError {
field: String!
message: String!
type: String
}
# A wrapper object around [FieldError] for use in interfaces and unions.
type FieldErrors {
fieldErrors: [FieldError!]!
}+
一个这样的例子就是这个
addPhoneNumber
突变
extend type Mutation {
# All Mutations that can be applied to a User
user: UserMutation!
}
union AddPhoneNumberResult = FieldErrors | MutationResult
type UserMutation {
# Add phone number to the user's account
addPhoneNumber(phoneNumber: String!): AddPhoneNumberResult! @RequireAuthorization(secure: true)
# Several other mutations...
我想更改此突变的返回类型以包含有关结果的更多详细信息,如下所示。
union AddPhoneNumberResult = FieldErrors | PhoneNumber
type PhoneNumber {
# A phone number associated with this account
phoneNumber: String!
# Whether or not this phone number has been verified via the SMS verification code
verified: Boolean!
factorStatus: FactorStatus!
}
enum FactorStatus {
PENDING_ACTIVATION,
ACTIVE,
EXPIRED
}
但是,我发现这对于任何调用此突变的客户端来说都是一个重大更改。请求正文必然会发生变化。
mutation AddPhoneNumber {
user {
addPhoneNumber(phoneNumber: "8765309") {
... on FieldErrors {
fieldErrors {
field
message
}
}
... on MutationResult {
success
}
}
}
}
到
mutation AddPhoneNumber {
user {
addPhoneNumber(phoneNumber: "8765309") {
... on FieldErrors {
fieldErrors {
field
message
}
}
... on PhoneNumber {
phoneNumber
verified
factorStatus
}
}
}
}
我们将更改部署到 GraphQL 服务的那一刻。由于
MutationResult
被许多其他突变使用,因此更改它本质上是添加一个没有其他突变可以填充的可为空字段。我能想到的任何其他对响应的改变都会落入同样的陷阱。
我认为进行此更改的唯一向后兼容的方法是引入一个全新的突变并将其单独连接,而单独保留
addPhoneNumber
突变。我能找到的所有关于向后兼容性的建议似乎都集中在系统设计时考虑到粒度类型的查询上。
还有什么我没看到的吗?
com.graphql-java
的 Java 项目,如果这在策略上有所不同的话。
您可以参考 GitLab,了解处理 GraphQL 中重大更改的真实示例。
基本上这个想法是你需要一步一步地去做。首先,让
AddPhoneNumberResult
可以支持所有可能的工会成员:
union AddPhoneNumberResult = FieldErrors | MutationResult | PhoneNumber
然后通知API用户
MutationResult
迟早会被删除。给他们适当的时间来更新代码以使用新的工会成员。否则,如果服务器有一些更新,他们可能会面临应用程序突然无法运行的风险。
在服务器端,您可以考虑监控
MutationResult
的使用情况,并在您觉得它的使用量足够低时实际删除它。
P.S
@deprecated
允许记录某个字段已弃用的原因。大多数 GraphQL 工具都会检测到它,并在用户尝试使用已弃用的字段时向用户发出警告。但它只能应用于字段和枚举值。之前有一个讨论使它也可以与工会成员一起使用,但我不知道它的最新状态。仅供参考。
针对需要相同处理的不同查询重新审视此问题,Ken Chan 的答案对于架构更改来说是正确的。困难的部分是检测用户请求的结果。
具体实现当然取决于服务器运行的语言和GQL库。正如我提到的,该服务器正在运行
graphql-java
,这是一个以 java 为中心的解决方案。我编写了一个实用方法,通过搜索实体
/**
* Searches the client's query for the named entity
*
* @param environment the GraphQL data fetching environment passed to each data fetcher
* @param entityName the name of the entity to search for
* @return true if the entity of the given name appears in the query
*/
public static boolean entityRequested(
DataFetchingEnvironment environment, @NotNull String entityName) {
var selectionSet = environment.getOperationDefinition().getSelectionSet().getSelections();
for (Selection selection : selectionSet) {
if (searchSelection(selection, entityName)) return true;
}
return false;
}
private static boolean searchSelection(Selection selection, String entityName) {
if (selection instanceof Field) {
if (searchField((Field) selection, entityName)) {
return true;
}
}
if (selection instanceof FragmentSpread) {
if (searchFragmentSpread((FragmentSpread) selection, entityName)) {
return true;
}
}
if (selection instanceof InlineFragment) {
if (searchInlineFragment((InlineFragment) selection, entityName)) {
return true;
}
}
return false;
}
public static boolean searchField(Field field, String entityName) {
if (entityName.equals(field.getName())) {
return true;
}
var selectionSet = field.getSelectionSet();
if (null == selectionSet) {
return false;
}
for (Selection selection : selectionSet.getSelections()) {
if (searchSelection(selection, entityName)) return true;
}
return false;
}
public static boolean searchFragmentSpread(FragmentSpread fragment, String entityName) {
if (entityName.equals(fragment.getName())) {
return true;
}
for (Directive directive : fragment.getDirectives()) {
if (entityName.equals(directive.getName())) return true;
}
return false;
}
public static boolean searchInlineFragment(InlineFragment fragment, String entityName) {
if (entityName.equals(fragment.getTypeCondition().getName())) {
return true;
}
var selectionSet = fragment.getSelectionSet();
if (null == selectionSet) {
return false;
}
for (Selection selection : selectionSet.getSelections()) {
if (searchSelection(selection, entityName)) return true;
}
return false;
}
这对于响应类型来说是多余的。实现后,我意识到返回类型始终是
FragmentSpread
对象类型,与突变中的 ...on
语法相对应。
您可以使用 DataFetcher 中的实用程序,使用varv.either 库,就像这样
@Component
public class AddPhoneNumberMutation implements DataFetcher<Object> {
@Override
public Object get(DataFetchingEnvironment environment) {
Either<FieldError, AddPhoneNumberResult> addPhoneNumberResult =
if (addPhoneNumberResult.isLeft()) {
return new FieldErrors(addPhoneNumberResult.getLeft());
}
if (entityRequested(environment, "PhoneNumber")) {
// If the query included the PhoneNumber entity return the new response type
return addPhoneNumberResult.get();
} else {
// Else, return the "basic" boolean MutationResult
return MutationResult.success();
}
}
}