GraphQL Java - Execution

Query查詢

在一個schema上執行查詢,須要首先建立一個GraphQL對象,而後調用該對象的execute()方法java

GraphQL在執行結束後返回一個ExecutionResult對象,其中包含查詢的數據(data字段)或錯誤信息(errors字段)。git

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。github

通常,咱們可使用PropertyDataFetcher對象,從內存中的POJO對象中提取field的值。若是你沒有爲一個field顯式指定一個DataFetcher,那麼GraphQL默認會使用PropertyDataFetcher與該field進行綁定。
但對於最頂層的領域對象(domain object)查詢來講,你須要定義一個特定的data fetcher。頂層的領域對象查詢,可能會包含數據庫操做,或經過HTTP協議與其餘系統進行交互得到相應數據。
GraphQL - Java並不關心你是如何獲取領域對象數據的,這是業務代碼中須要考慮的問題。它也不關心在獲取數據時須要怎樣的認證方式,你須要在業務層代碼中實現這部分邏輯。數據庫

一個簡單的Data Fetcher示例以下:json

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容許返回部分紅功的數據,並帶上異常信息。promise

正常的異常處理邏輯以下:緩存

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對象當中。app

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異常處理器改變它的行爲,只須要在執行策略中註冊一下。dom

例如,上述代碼記錄了底層的異常和調用棧信息,若是你不但願這些信息出如今輸出的錯誤列表中,能夠用一下的方法實現。

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"
    }
相關文章
相關標籤/搜索