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<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对象,那么该对象也会被整合到整个异步查询的过程当中。这样,可以同时发起多个data fetch操作,各操作之间并行运行。
下面的代码中使用了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,提高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();
  1. 创建一个cache的实例,示例代码使用的是caffeine的缓存方案。
  2. PreparedDocumentProvider是一个FunctionInterface(Java8特性),仅仅提供了一个get方法。

若开启了缓存,那么查询语句中不能显式的拼接查询条件的值。而应该以变量的方式进行传递。例如:

    query HelloTo {
         sayHello(to: "Me") {
            greeting
         }
    }

这个查询语句,重写如下:

    query HelloTo($to: String!) {
         sayHello(to: $to) {
            greeting
         }
    }
    # 传入参数如下:
    {
       "to": "Me"
    }
 类似资料: