GraphQL Java - Execution
Query查询
在一个schema上执行查询,需要首先创建一个GraphQL
对象,然后调用该对象的execute()
方法
GraphQL在执行结束后返回一个ExecutionResult
对象,其中包含查询的数据(data字段)或错误信息(errors字段)。
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.build();
GraphQL graphQL = GraphQL.newGraphQL(schema)
.build();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
.build();
ExecutionResult executionResult = graphQL.execute(executionInput);
Object data = executionResult.getData();
List<GraphQLError> errors = executionResult.getErrors();
Data Fetcher
每个GraphQL中的field(字段)都会绑定一个DataFetcher。在其他的GraphQL实现中,也称DataFetcher为Resolver。
一般,我们可以使用PropertyDataFetcher对象,从内存中的POJO对象中提取field的值。如果你没有为一个field显式指定一个DataFetcher,那么GraphQL默认会使用PropertyDataFetcher与该field进行绑定。
但对于最顶层的领域对象(domain object)查询来说,你需要定义一个特定的data fetcher。顶层的领域对象查询,可能会包含数据库操作,或通过HTTP协议与其他系统进行交互获得相应数据。
GraphQL - Java并不关心你是如何获取领域对象数据的,这是业务代码中需要考虑的问题。它也不关心在获取数据时需要怎样的认证方式,你需要在业务层代码中实现这部分逻辑。
一个简单的Data Fetcher示例如下:
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
return fetchUserFromDatabase(environment.getArgument("userId"));
}
};
每个DataFetcher的方法中,都会传入一个DataFetchingEnvironment对象。这个对象中包含了当前正在被请求的field,field所关联的请求参数argument,以及其他信息(例如,当前field的上层field、当前查询的root对象或当前查询的context对象等)。
在上面的例子中,GraphQL会在data fetcher返回执行结果前一直等待,这是一种阻塞的调用方式。也可以通过返回data相关的CompletionStage对象,将DataFetcher的调用异步化,实现异步调用。
数据获取时产生的异常
如果在GraphQL的DataFetcher执行过程中产生了异常,在GraphQL的执行策略下, 将生成一个ExceptioinWhileDataFetching错误对象,并将它添加到返回的ExecutionResult对象的errors列表字段当中。GraphQL允许返回部分成功的数据,并带上异常信息。
正常的异常处理逻辑如下:
public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(SimpleDataFetcherExceptionHandler.class);
@Override
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
Throwable exception = handlerParameters.getException();
SourceLocation sourceLocation = handlerParameters.getField().getSourceLocation();
ExecutionPath path = handlerParameters.getPath();
ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, exception, sourceLocation);
handlerParameters.getExecutionContext().addError(error);
log.warn(error.getMessage(), exception);
}
}
如果抛出的异常是一个GraphqlError对象,那么它会将异常信息和扩展属性转换到ExceptionWhileDataFetching对象。可以把自己的错误信息,放到GraphQL的错误列表当中返回给调用方。
例如,假设data fetcher抛出了如下异常,那么foo和fizz属性会包含在graphql error对象当中。
class CustomRuntimeException extends RuntimeException implements GraphQLError {
@Override
public Map<String, Object> getExtensions() {
Map<String, Object> customAttributes = new LinkedHashMap<>();
customAttributes.put("foo", "bar");
customAttributes.put("fizz", "whizz");
return customAttributes;
}
@Override
public List<SourceLocation> getLocations() {
return null;
}
@Override
public ErrorType getErrorType() {
return ErrorType.DataFetchingException;
}
}
可以编写自己的DataFetcherExceptionHandler异常处理器改变它的行为,只需要在执行策略中注册一下。
例如,上述代码记录了底层的异常和调用栈信息,如果你不希望这些信息出现在输出的错误列表中,可以用一下的方法实现。
DataFetcherExceptionHandler handler = new DataFetcherExceptionHandler() {
@Override
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
//
// do your custom handling here. The parameters have all you need
}
};
ExecutionStrategy executionStrategy = new AsyncExecutionStrategy(handler);
返回数据和异常
也可以在一个DataFetcher中同时返回数据和多个error信息,只需要让DataFetcher返回DataFetcherResult对象或CompletableFuture包装后的DataFetcherResult对象即可。
在某些场景下,例如DataFetcher需要从多个数据源或其他的GraphQL系统中获取数据时,其中任一环节都可能产生错误。使用DataFetcherResult包含data和期间产生的所有error信息,比较常见。
下面的示例中,DataFetcher从另外的GraphQL系统中获取数据,并返回执行的data和errors信息。
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
Map response = fetchUserFromRemoteGraphQLResource(environment.getArgument("userId"));
List<GraphQLError> errors = response.get("errors")).stream()
.map(MyMapGraphQLError::new)
.collect(Collectors.toList();
return new DataFetcherResult(response.get("data"), errors);
}
};
序列化返回结果为json格式
通常,使用Jackson或GSON的json序列化库,将返回查询结果序列化为json格式返回。然而对于如何序列化数据,序列化为JSON后会保留哪些信息,取决于序列化库自身。例如,对于null结果是否出现在序列化后的json数据当中,不同的序列化库有不同的默认策略。需要手动指定json mapper来定义。
为了保证可以100%获取一个符合graphql规范的json结果,可以在返回结果result上调用toSpecification,然后将数据以json格式返回。
ExecutionResult executionResult = graphQL.execute(executionInput);
Map<String, Object> toSpecificationResult = executionResult.toSpecification();
sendAsJson(toSpecificationResult);
Mutation(更新)
首先,需要定义一个支持输入参数的GraphQLObjectType
类型,该类型也是Mutation方法的参数类型。这些参数会在data fetcher调用时,更新GraphQL系统内部的领域数据信息(添加、修改或删除)。
mutation的执行调用示例如下:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
在mutation方法执行过程中需要传递参数,例如实例中,需要传递$ep和$review变量。
在Java代码中,可以使用如下方式创建type,并绑定这个mutation操作。
GraphQLInputObjectType episodeType = newInputObject()
.name("Episode")
.field(newInputObjectField()
.name("episodeNumber")
.type(Scalars.GraphQLInt))
.build();
GraphQLInputObjectType reviewInputType = newInputObject()
.name("ReviewInput")
.field(newInputObjectField()
.name("stars")
.type(Scalars.GraphQLString)
.name("commentary")
.type(Scalars.GraphQLString))
.build();
GraphQLObjectType reviewType = newObject()
.name("Review")
.field(newFieldDefinition()
.name("stars")
.type(GraphQLString))
.field(newFieldDefinition()
.name("commentary")
.type(GraphQLString))
.build();
GraphQLObjectType createReviewForEpisodeMutation = newObject()
.name("CreateReviewForEpisodeMutation")
.field(newFieldDefinition()
.name("createReview")
.type(reviewType)
.argument(newArgument()
.name("episode")
.type(episodeType)
)
.argument(newArgument()
.name("review")
.type(reviewInputType)
)
)
.build();
GraphQLCodeRegistry codeRegistry = newCodeRegistry()
.dataFetcher(
coordinates("CreateReviewForEpisodeMutation", "createReview"),
mutationDataFetcher()
)
.build();
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.mutation(createReviewForEpisodeMutation)
.codeRegistry(codeRegistry)
.build();
注意,输入参数只能是GraphQLInputObjectType类型,不能是可以作为输出类型的GraphQLObjectType。
另外,Scalar类型比较特殊,可以同时作为输入参数类型和输出类型。
Mutation操作绑定的data fetcher可以执行这个mutation,并返回输出类型的数据信息:
private DataFetcher mutationDataFetcher() {
return new DataFetcher() {
@Override
public Review get(DataFetchingEnvironment environment) {
//
// The graphql specification dictates that input object arguments MUST
// be maps. You can convert them to POJOs inside the data fetcher if that
// suits your code better
//
// See http://facebook.github.io/graphql/October2016/#sec-Input-Objects
//
Map<String, Object> episodeInputMap = environment.getArgument("episode");
Map<String, Object> reviewInputMap = environment.getArgument("review");
//
// in this case we have type safe Java objects to call our backing code with
//
EpisodeInput episodeInput = EpisodeInput.fromMap(episodeInputMap);
ReviewInput reviewInput = ReviewInput.fromMap(reviewInputMap);
// make a call to your store to mutate your database
Review updatedReview = reviewStore().update(episodeInput, reviewInput);
// this returns a new view of the data
return updatedReview;
}
};
}
如上所示,方法调用了数据库操作变更了后端的数据存储信息,然后返回一个Review类型对象返回给mutation的调用方。
异步执行
graphql-java使用了完全异步化的执行策略,调用executeAsync()后,返回CompleteableFuture
对象
GraphQL graphQL = buildSchema();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
.build();
CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
promise.thenAccept(executionResult -> {
// here you might send back the results as JSON over HTTP
encodeResultToJsonAndSendResponse(executionResult);
});
promise.join();
使用CompletableFuture对象,可以指定该执行结果结束后需要出发的后续行为或操作,最后调用.join()方法等待执行完成。
实际上,使用GraphQL Java执行execute的同步操作,也是在调用异步的executeAsync方法之后,再调用join方法实现的。
ExecutionResult executionResult = graphQL.execute(executionInput);
// the above is equivalent to the following code (in long hand)
CompletableFuture<ExecutionResult> promise = graphQL.executeAsync(executionInput);
ExecutionResult executionResult2 = promise.join();
如果DataFetcher返回了CompletableFuture
下面的代码中使用了Java中的ForkJoinPool.commonPool线程池,提供异步执行操作流程。
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
CompletableFuture<User> userPromise = CompletableFuture.supplyAsync(() -> {
return fetchUserViaHttp(environment.getArgument("userId"));
});
return userPromise;
}
};
上述代码在Java8中也可以重构如下:
DataFetcher userDataFetcher = environment -> CompletableFuture.supplyAsync(
() -> fetchUserViaHttp(environment.getArgument("userId")));
Graphql - Java会保证所有的CompletableFuture对象组合执行,并依照GraphQL规范返回执行结果。
在GraphQL - Java中也可以使用AsyncDataFetcher.async(DataFetcher
DataFetcher userDataFetcher = async(environment -> fetchUserViaHttp(environment.getArgument("userId")));
执行策略
在执行query或mutation时,GraphQL Java引擎会使用ExecutionStrategy接口的具体实现类(执行策略)。GraphQL - Java提供了一些策略,你也可以编写自己的执行策略。
可以在创建GraphQL
对象时中绑定执行策略。
GraphQL.newGraphQL(schema)
.queryExecutionStrategy(new AsyncExecutionStrategy())
.mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
.build();
实际上,上述代码等价于不指定ExecutionStrategy的默认策略。
AsyncExecutionStrategy
query操作的默认执行策略是AsyncExecutionStrategy。在这个执行策略下,GraphQL Java引擎将field的返回结果包装为CompleteableFuture对象(如果返回结果本身为CompletableFuture对象,则不处理),哪个field的值获取操作先完成并不重要。
若data fetcher调用本身返回的就是CompletationStage类型,则可以最大化异步调用的性能。
对于如下的query:
query {
hero {
enemies {
name
}
friends {
name
}
}
}
AsyncExecutionStrategy会在获取friends字段值的同时,调用获取enemies字段值的方法。而不会在获取enemies之后获取friends字段的值,以提升效率。
在执行结束后,GraphQL Java会将查询结果按照请求的顺序进行整合。查询结果遵循Graphql规范,并且返回的field对象按照查询的field字段的顺序返回。
执行过程中,仅仅是field字段的执行顺序是任意的,返回的查询结果依然是顺序的。
AsyncSerialExecutionStrategy
GraphQL 规范要求mutation操作必须按照query的field顺序依次执行。
因此,AsyncSerialExecutionStrategy是mutation的默认策略,并且它会保证每个field在下一个field操作开始之前完成。
你仍然可以在mutation类型的data fetcher中返回CompletionStage,但它们只会顺序依次执行。
SubscriptionExecutionStrategy
略。
Query缓存
在GraphQL Java执行查询之前,查询语句首先应该进行解析和验证,这个过程有时候会非常耗时。
为了避免重复遍历、验证查询语句,GraphQL.Builder允许引入PreparedDocumentProvider,来重用相同query语句的Document解析实例。
这个过程只是对Document进行缓存,并未对查询的执行结果进行缓存。
Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build(); (1)
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
.preparsedDocumentProvider(cache::get) (2)
.build();
- 创建一个cache的实例,示例代码使用的是caffeine的缓存方案。
- PreparedDocumentProvider是一个FunctionInterface(Java8特性),仅仅提供了一个get方法。
若开启了缓存,那么查询语句中不能显式的拼接查询条件的值。而应该以变量的方式进行传递。例如:
query HelloTo {
sayHello(to: "Me") {
greeting
}
}
这个查询语句,重写如下:
query HelloTo($to: String!) {
sayHello(to: $to) {
greeting
}
}
# 传入参数如下:
{
"to": "Me"
}