java execution_Graphql的执行

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值