GraphQL Java - Execution


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"
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM