• GraphQL Java


    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"
        }
    
  • 相关阅读:
    Go基础系列:双层channel用法示例
    shell脚本动画小工具
    Go基础系列:channel入门
    python面向对象入门(1):从代码复用开始
    python包导入细节
    python模块导入细节
    Go基础系列:读取标准输入
    黄聪:C#获取网页HTML内容的三种方式
    黄聪:如何正确在Vue框架里使用Swiper
    黄聪:C#使用GeckoFx拦截监控Http数据
  • 原文地址:https://www.cnblogs.com/pku-liuqiang/p/11502881.html
Copyright © 2020-2023  润新知