Graphql的执行
查询(Queries)
要执行一个符合schema定义的查询,首先需通过schema对象创建一个GraphQL对象,然后调用execute()。
查询的结果是一个 ExecutionResult 对象,它包含查询数据列表或者是错误信息。
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 errors = executionResult.getErrors();
在StarWars查询测试中可以找到更复杂的查询示例
数据提取器 (Data Fetchers)
每个graphql字段类型都有一个graphql.schema.DataFetcher关联。
通常,如果您未在字段上指定DataFetcher,则graphql将使用默认的DataFetcher graphql.schema.PropertyDataFetcher从POJO对象中获取相应的属性值赋给graphql字段。
创建一个DataFetcher对象需要你自己去实现数据获取过程的代码,graphql-java 对如何获取数据对象,以及数据权限等等不关心,这是你需要关心的问题(可以和Java中接口概念作类比),这可能涉及进行数据库调用或通过HTTP语句联系另一个系统。
DataFetcher示例如下:
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
return fetchUserFromDatabase(environment.getArgument("userId"));
}
};
每个DataFetcher对象都传递一个graphql.schema.DataFetchingEnvironment对象,该对象包含要获取的字段、向该字段提供了哪些参数以及其他信息,例如字段的父对象,查询根对象或查询上下文对象。
在上面的示例中,执行是同步的,它将等待一个DataFetcher返回数据后再继续。您可以在DataFetcher中通过返回一个CompletionStage对象到数据来达到执行异步的效果,这将在本文中进一步说明。
数据提取时的异常
如果在DataFetcher调用期间发生异常,默认情况下,执行策略将产生一个 graphql.ExceptionWhileDataFetching错误并将其添加到结果错误列表中。请记住,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对象中。这使您可以将自己的自定义属性放入GraphqlError中发送回给DataFetcher的调用方。
例如,假设您的DataFetcher引发了此异常。在foo和fizz属性将包含在最终的GraphqlError中:
class CustomRuntimeException extends RuntimeException implements GraphQLError {
@Override
public Map getExtensions() {
Map customAttributes = new LinkedHashMap<>();
customAttributes.put("foo", "bar");
customAttributes.put("fizz", "whizz");
return customAttributes;
}
@Override
public List getLocations() {
return null;
}
@Override
public ErrorType getErrorType() {
return ErrorType.DataFetchingException;
}
}
您可以通过创建自己的graphql.execution.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可以通过graphql.execution.DataFetcherResult直接返回数据,可以同时返回数据和多个错误,或者把数据包装在CompletableFuture实例中返回来达到异步执行效果。当DataFetcher可能需要从多个来源或另一个GraphQL资源检索数据时,此功能很有用。
在此示例中,DataFetcher从另一个GraphQL资源中检索用户并返回其数据和错误。
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
Map response = fetchUserFromRemoteGraphQLResource(environment.getArgument("userId"));
List errors = response.get("errors")).stream()
.map(MyMapGraphQLError::new)
.collect(Collectors.toList();
return new DataFetcherResult(response.get("data"), errors);
}
};
将结果序列化为JSON
调用graphql的最常见做法是通过HTTP调用并返回JSON格式数据。因此,您需要将graphql.ExecutionResult对象转变成 JSON。
常用的转变方法是使用JSON序列化库,例如Jackson或GSON。但是,它们对数据结果的格式有它们特定要求。例如nulls,在graphql结果中很重要,因此您必须确保所使用的json映射器能够解析这样的数据。
为了确保您获得100%符合graphql规范的JSON结果,您应该调用toSpecification方法,然后将其作为JSON发送回去。
ExecutionResult executionResult = graphQL.execute(executionInput);
Map toSpecificationResult = executionResult.toSpecification();
sendAsJson(toSpecificationResult);
编辑修改(Mutations)
本质上,您还是需要定义一个修改(mutation)的GraphQLObjectType(参考有关schema的章节),这个GraphQLObjectType一般需定义输入的参数,这些参数是您在DataFetcher中进行修改操作或实现修改的业务逻辑所需要的。
修改是通过类似以下查询的方式调用的:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
您需要在该修改操作期间传参数,在上面例子中,您需要为变量$ep和变量$review提供值。
您还可以通过创建类似下面这样的代码来实现此修改操作:
GraphQLInputObjectType episodeType = GraphQLInputObjectType.newInputObject()
.name("Episode")
.field(newInputObjectField()
.name("episodeNumber")
.type(Scalars.GraphQLInt))
.build();
GraphQLInputObjectType reviewInputType = GraphQLInputObjectType.newInputObject()
.name("ReviewInput")
.field(newInputObjectField()
.name("stars")
.type(Scalars.GraphQLString)
.name("commentary")
.type(Scalars.GraphQLString))
.build();
GraphQLObjectType reviewType = GraphQLObjectType.newObject()
.name("Review")
.field(newFieldDefinition()
.name("stars")
.type(GraphQLString))
.field(newFieldDefinition()
.name("commentary")
.type(GraphQLString))
.build();
GraphQLObjectType createReviewForEpisodeMutation = GraphQLObjectType.newObject()
.name("CreateReviewForEpisodeMutation")
.field(newFieldDefinition()
.name("createReview")
.type(reviewType)
.argument(newArgument()
.name("episode")
.type(episodeType)
)
.argument(newArgument()
.name("review")
.type(reviewInputType)
)
)
.build();
GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
.dataFetcher(
FieldCoordinates.coordinates("CreateReviewForEpisodeMutation", "createReview"),
mutationDataFetcher()
)
.build();
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(queryType)
.mutation(createReviewForEpisodeMutation)
.codeRegistry(codeRegistry)
.build();
请注意,输入参数的类型一般为GraphQLInputObjectType,这个很重要,输入参数不能使用输出类型,例如GraphQLObjectType。不过,标量类型可以作为输入参数的类型,也可以作为输出类型。
此处的DataFetcher程序负责执行修改操作并返回一些相应的输出值:
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 episodeInputMap = environment.getArgument("episode");
Map 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对象作为输出值,或者叫返回值。
异步执行
graphql-java在执行查询时使用完全异步执行技术。你可以像下面的代码一样,通过调用 executeAsync()得到一个CompleteableFuture对象结果:
GraphQL graphQL = buildSchema();
ExecutionInput executionInput = ExecutionInput.newExecutionInput().query("query { hero { name } }")
.build();
CompletableFuture 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()通过为您调用join使方法显得同步。因此以下代码实际上是相同的。
ExecutionResult executionResult = graphQL.execute(executionInput);
// 上面的代码和下面的代码效果是一样的 (这里的代码都只是示意代码)
CompletableFuture promise = graphQL.executeAsync(executionInput);
ExecutionResult executionResult2 = promise.join();
如果graphql.schema.DataFetcher返回了一个CompletableFuture对象,则这个对象将组合进整个的异步查询执行中。这意味着您可以并行触发多个字段获取请求。究竟使用哪种线程策略取决于DataFetcher的程序代码。
以下代码使用标准Javajava.util.concurrent.ForkJoinPool.commonPool()线程执行程序在另一个线程中提供值。
DataFetcher userDataFetcher = new DataFetcher() {
@Override
public Object get(DataFetchingEnvironment environment) {
CompletableFuture userPromise = CompletableFuture.supplyAsync(() -> {
return fetchUserViaHttp(environment.getArgument("userId"));
});
return userPromise;
}
};
上面的代码以长格式编写,使用Java 8 lambda表达式,可以更简洁地编写如下
DataFetcher userDataFetcher = environment -> CompletableFuture.supplyAsync(
() -> fetchUserViaHttp(environment.getArgument("userId")));
graphql-java引擎确保将所有CompletableFuture对象组合在一起,以提供遵循graphql规范的执行结果。
graphql-java中有一个有用的快捷方式来创建异步DataFetcher程序。使用graphql.schema.AsyncDataFetcher.async(DataFetcher)包裹一个 DataFetcher。可以将其与静态导入一起使用,以产生更具可读性的代码。
DataFetcher userDataFetcher = async(environment -> fetchUserViaHttp(environment.getArgument("userId")));
执行策略(Execution Strategies)
一个派生自graphql.execution.ExecutionStrategy的策略类,可以用于运行查询或修改。graphql-java提供了许多不同的策略,如果您真的很热衷策略,甚至可以编写自己的策略。
您可以在创建GraphQL对象时确定要使用的执行策略。
GraphQL.newGraphQL(schema)
.queryExecutionStrategy(new AsyncExecutionStrategy())
.mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
.build();
实际上,上面的代码等效于默认设置,并且在大多数情况下是执行策略的非常明智的选择。
异步执行策略(AsyncExecutionStrategy)
默认情况下,“查询”的执行策略是graphql.execution.AsyncExecutionStrategy,它将每个字段分配为CompleteableFuture对象,但不关心哪个字段最先完成。此策略可实现最佳性能。
被调用的DataFetcher程序本身可以返回CompletionStage值,这将创建完全异步的行为。
因此,想象一个查询如下
query {
hero {
enemies {
name
}
friends {
name
}
}
}
该AsyncExecutionStrategy将同时自由的调度enemies和friends的字段。而不是先完成enemies的调度,然后再调度friends,这将是效率较低的。
但是它将按顺序组合结果。查询结果将遵循graphql规范,并返回按查询字段顺序组合的对象值。只是数据获取的执行顺序可以任意自由进行。
graphql规范中允许这种行为,并且实际上是被积极鼓励的。http://facebook.github.io/graphql/#sec-Query 用于只读查询。
详细信息,请参见specification 。
异步串行执行策略(AsyncSerialExecutionStrategy)
graphql规范指出,修改的执行必须按查询字段出现的顺序执行。
因此,修改(mutations)的执行策略默认是graphql.execution.AsyncSerialExecutionStrategy,它确保一个字段执行完成后才能执行下一个字段。您仍然可以在修改的DataFetcher中用CompletionStage返回数据对象,但是它们将被串行执行,并在调用下一个修改字段的DataFetcher之前完成。
订阅执行策略(SubscriptionExecutionStrategy)
Graphql订阅允许您创建对graphql数据的有状态订阅。SubscriptionExecutionStrategy 将会被用作执行策略,因为它具有对反应流API的支持。
有关反应式Publisher和Subscriber接口的更多信息,请参见http://www.reactive-streams.org/。
另请参阅订阅页面,以获取有关如何编写基于订阅的graphql服务的更多详细信息。
查询缓存(Query Caching)
在graphql-java引擎执行查询之前,会先对其进行解析和验证,此过程可能会很耗时。
为了避免需要重新解析/验证,GraphQL.Builder允许的实例PreparsedDocumentProvider重用Document实例。
请注意,这不会缓存查询结果,仅缓存已解析的Document。
Cache cache = Caffeine.newBuilder().maximumSize(10_000).build(); (1)
PreparsedDocumentProvider preparsedCache = PreparsedDocumentProvider {
@Override
public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, Function computeFunction) {
Function mapCompute = key -> computeFunction.apply(executionInput);
return cache.get(executionInput.getQuery(), mapCompute);
}
}
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
.preparsedDocumentProvider(preparsedCache) (2)
.build();创建首选缓存实例的实例,这里使用Caffeine 作为高质量的缓存解决方案。缓存实例应该是线程安全的并且是共享的。
PreparsedDocumentProvider是仅具有getDocument方法的接口,它的作用是获得一个预解析的查询缓存,如果不存在,则computeFunction被调用去解析和验证该查询。
为了获得较高的高速缓存命中率,建议将字段参数作为变量而不是直接在查询中传递。
以下查询:
query HelloTo {
sayHello(to: "Me") {
greeting
}
}
应该改写为
query HelloTo($to: String!) {
sayHello(to: $to) {
greeting
}
}
带有变量:
{
"to": "Me"
}
现在,无论提供了什么变量值,都可以重用该查询。
更新时间 1 Jan 2021