graphql java date_GraphQL Java - Execution

公孙茂学
2023-12-01

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 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 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;

}

}

可以编写自己的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 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 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 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类型对象返回给mutation的调用方。

异步执行

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的同步操作,也是在调用异步的executeAsync方法之后,再调用join方法实现的。

ExecutionResult executionResult = graphQL.execute(executionInput);

// the above is equivalent to the following code (in long hand)

CompletableFuture promise = graphQL.executeAsync(executionInput);

ExecutionResult executionResult2 = promise.join();

如果DataFetcher返回了CompletableFuture对象,那么该对象也会被整合到整个异步查询的过程当中。这样,可以同时发起多个data fetch操作,各操作之间并行运行。

下面的代码中使用了Java中的ForkJoinPool.commonPool线程池,提供异步执行操作流程。

DataFetcher userDataFetcher = new DataFetcher() {

@Override

public Object get(DataFetchingEnvironment environment) {

CompletableFuture 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,提高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 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"

}

 类似资料: