graphql-java使用手冊:part3 執行(Execution)

原文:http://blog.mygraphql.com/wordpress/?p=102java

執行(Execution)

查詢(Queries)

爲了對 一個Schema 執行查詢。須要先構造一個 GraphQL
對象,並帶着一些參數去調用 execute() 方法.react

查詢將返回一個 ExecutionResult 對象,其中包含查詢的結果數據
(或出錯時的錯誤信息集合).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();

更復雜的示例,能夠看 StarWars 查詢測試用例github

Data Fetchers

每一個graphql schema 中的field,都須要綁定相應的
graphql.schema.DataFetcher 以獲取數據. 其它GraphQL的實現把這叫
resolvers*.express

不少時候,你能夠用默認的 graphql.schema.PropertyDataFetcher 去從 Java
POJO 中自動提取數據到對應的 field. 若是你未爲 field 指定 data fetcher
那麼就默認使用它.json

但你最少須要爲頂層的領域對象(domain objects) 編寫 data fetchers.
其中能夠會與database交互,或用HTTP與其它系統交互.數組

graphql-java 不關心你如何獲取你的業務數據,這是你的本身.
它也不關心你若是受權你的業務數據.
你應該在本身的業務邏輯層,去實現這些邏輯.promise

簡單 Data fetcher 示例:緩存

DataFetcher userDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        return fetchUserFromDatabase(environment.getArgument("userId"));
    }
};

框架在執行查詢時。會調用上面的方法,其中的
graphql.schema.DataFetchingEnvironment 參數包括如下信息:被查詢的
field、查詢這個field時可能帶上的查詢參數、這個field的父數據對象(Source
Object)、 查詢的ROOT數據對象、查詢執行上下文環境對象(query context
object).安全

上面是同步獲取數據的例子,執行引擎須要等待一個 data fetcher
返回數據才能繼續下一個. 也能夠經過編寫異步的 DataFetcher ,異步地返回
CompletionStage 對象,在下文中將會說明使用方法.

當獲取數據出現異常時

若是異步是出如今調用 data fetcher 時, 默認的執行策略(execution strategy)
將生成一個 graphql.ExceptionWhileDataFetching
錯誤,並將其加入到查詢結果的錯誤列表中. 請留意,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 類型,框架會把其中的消息 和
自定義擴展屬性(custom extensions attributes)轉換到
ExceptionWhileDataFetching 對象中.
這能夠方便你把本身的錯誤信息,放到返回給調用者的 GraphQL 錯誤列表中.

例如,你在 DataFetcher 中拋出了這個異常. 那麼 foo and fizz
屬性將會包含在返回給調用者的graphql查詢錯誤中.

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

你能夠編寫本身的 graphql.execution.DataFetcherExceptionHandler
來改變這些邏輯。只須要在執行策略(execution strategy)註冊一下.

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

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

序列化成 JSON

一般,用 HTTP 方法去調用 graphql ,用 JSON 格式做爲返回結果.
返回,須要把 graphql.ExecutionResult 對象轉換爲 JSON 格式包.

通常用 Jackson or GSON 去作 JSON 序列化.
但他們對結果數據的轉換方法有一些不一樣點. 例如 JSON 的`nulls` 在 graphql
結果中的是有用的。因此必須在 json mappers 中設置須要它

爲保證你返回的 JSON 結果 100% 合符 graphql 規範, 應該調用result對象的
toSpecification 方法,而後以 JSON格式 發送響應.

這樣就能夠確保返回數據合符在
http://facebook.github.io/gra... 中的規範

ExecutionResult executionResult = graphQL.execute(executionInput);

Map<String, Object> toSpecificationResult = executionResult.toSpecification();

sendAsJson(toSpecificationResult);

更新(Mutations)

若是你不瞭解什麼叫更新(Mutations),建議先閱讀規範
http://graphql.org/learn/quer....

首先,你須要定義一個支持輸入參數的 GraphQLObjectType .
在更新數據時,框架會帶上這些參數去調用 data fetcher.

下面是,GraphQL 更新語句的例子 :

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

修改操做是須要帶輸入參數的,上例中對應變量 $ep and $review

對應地,Schema 應該這麼寫【譯註:如下是 Java 寫法,你也能夠用SDL寫法】 :

GraphQLInputObjectType episodeType = GraphQLInputObjectType.newInputObject()
        .name("Episode")
        .field(newInputObjectField()
                .name("episodeNumber")
                .type(Scalars.GraphQLInt))
        .build();

GraphQLInputObjectType reviewInputType = GraphQLInputObjectType.newInputObject()
        .name("ReviewInput")
        .field(newInputObjectField()
                .name("stars")
                .type(Scalars.GraphQLString))
        .field(newInputObjectField()
                .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)
                )
                .dataFetcher(mutationDataFetcher())
        )
        .build();

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(queryType)
        .mutation(createReviewForEpisodeMutation)
        .build();

注意,輸入參數應該是 GraphQLInputObjectType 類型. 請留意.
對於修改操做,輸入參數只能用這個類型(type),而不能用如
>><<GraphQLObjectType之類的輸出類型(type). Scalars 類型(type)
能夠用於輸入和輸出.

對於更新操做,DataFetcher的職責是執行數據更新行返回執行結果.

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 對象給調用方.

異步執行(Asynchronous Execution)

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
對象,你能夠指定,在查詢完成後,組合其它操做(action)或函數你的函數.
須要你須要同步等待執行結果 ,能夠調用 .join() 方法.

graphql-java引擎內部是異步執行的,但你能夠經過調用 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();

若是你編寫的 graphql.schema.DataFetcher 返回 CompletableFuture<T>
對象,那麼它會被糅合到整個異步查詢中.
這樣,你能夠同時發起我個數據獲取操做,讓它們並行運行.
而由DataFetcher控制具體的線程併發策略.

下面示例使用 java.util.concurrent.ForkJoinPool.commonPool()
並行執行器,用其它線程完成數據獲取.

DataFetcher userDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        CompletableFuture<User> userPromise = CompletableFuture.supplyAsync(() -> {
            return fetchUserViaHttp(environment.getArgument("userId"));
        });
        return userPromise;
    }
};

上面是舊的寫法,也能夠用Java 8 lambdas 的寫法:

DataFetcher userDataFetcher = environment -> CompletableFuture.supplyAsync(
        () -> fetchUserViaHttp(environment.getArgument("userId")));

graphql-java 保證全部 CompletableFuture 對象組合,最後生成合符 graphql
規範的執行結果.

還有一個方法能夠簡化異步 data fetchers 的編寫. 使用
graphql.schema.AsyncDataFetcher.async(DataFetcher<T>)
去包裝DataFetcher. 這樣可使用 static imports 來提升代碼可讀性.

DataFetcher userDataFetcher = async(environment -> fetchUserViaHttp(environment.getArgument("userId")));

關於執行策略(Execution Strategies)

在執行查詢或更新數據時,引擎會使用實現了
>><<graphql.execution.ExecutionStrategy接口 的對象,來決定執行策略.
graphql-java 中已經有幾個現成的策略,但若是你須要,你能夠寫本身的。.

你能夠這樣給 GraphQL 對象綁定執行策略。

GraphQL.newGraphQL(schema)
        .queryExecutionStrategy(new AsyncExecutionStrategy())
        .mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
        .build();

實際上,上面就是引擎默認的策略了。大部分狀況下用它就夠了。

異步執行策略(AsyncExecutionStrategy)

默認的查詢 執行策略是 graphql.execution.AsyncExecutionStrategy
,它會把每一個 field 返回視爲 CompleteableFuture 。它並不會控制 filed
的獲取順序. 這個策略能夠優化查詢執行的性能.

Data fetchers 返回 CompletionStage`
對象,就能夠全異步執行整個查詢了。

例如如下的查詢:

query {
  hero {
    enemies {
      name
    }
    friends {
      name
    }
  }
}

The AsyncExecutionStrategy is free to dispatch the enemies field at
the same time as the friends field. It does not have to do enemies
first followed by friends, which would be less efficient.

這個策略不會按順序來集成結果數據。但查詢結果會按GraphQL規範順序來返回。只是數據獲取的順序不肯定。

對於查詢,這個策略是 graphql 規範
http://facebook.github.io/gra... 容許和推薦的。

詳細見 規範 .

異步順序執行策略(AsyncSerialExecutionStrategy)

Graphql 規範指出,修改操做(mutations)「必須」按照 field 的順序來執行。

因此,爲了確保一個 field 一個 field
順序地執行更新,更新操做(mutations)默認使用
graphql.execution.AsyncSerialExecutionStrategy 策略。你的 mutation
Data Fetcher 仍然能夠返回 CompletionStage 對象, 但它和其它 field
的是串行執行的。

基於執行器的執行策略:ExecutorServiceExecutionStrategy

The graphql.execution.ExecutorServiceExecutionStrategy execution
strategy will always dispatch each field fetch in an asynchronous
manner, using the executor you give it. It differs from
AsyncExecutionStrategy in that it does not rely on the data fetchers
to be asynchronous but rather makes the field fetch invocation
asynchronous by submitting each field to the provided
java.util.concurrent.ExecutorService.

由於這樣,因此它不能用於更新(mutation)操做。

ExecutorService  executorService = new ThreadPoolExecutor(
        2, /* core pool size 2 thread */
        2, /* max pool size 2 thread */
        30, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(),
        new ThreadPoolExecutor.CallerRunsPolicy());

GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
        .queryExecutionStrategy(new ExecutorServiceExecutionStrategy(executorService))
        .mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
        .build();

訂閱執行策略(SubscriptionExecutionStrategy)

Graphql 訂閱(subscriptions) 使你能夠對GraphQL
數據進行爲狀態的訂閱。你可使用 SubscriptionExecutionStrategy
執行策略,它支持 reactive-streams APIs。

閱讀 http://www.reactive-streams.org/ 能夠獲得關於 Publisher
Subscriber 接口的更多信息。

也能夠閱讀subscriptions的文檔,以瞭解如何編寫基於支持訂閱的 graphql
服務。

批量化執行器(BatchedExecutionStrategy)

對於有數組(list)field 的 schemas, 咱們提供了
graphql.execution.batched.BatchedExecutionStrategy
策略。它能夠批量化地調用標註了@Batched 的 DataFetchers 的 get() 方法。

關於 BatchedExecutionStrategy
是如何工做的。它是如此的特別,讓我不知道如何解釋【譯註:原文:Its a
pretty special case that I don’t know how to explain properly】

控制字段的可見性

全部 GraphqlSchema
的字段(field)默認都是能夠訪問的。但有時候,你可能想不一樣用戶看到不一樣部分的字段。

你能夠在schema 上綁定一個
graphql.schema.visibility.GraphqlFieldVisibility 對象。.

框架提供了一個能夠指定字段(field)名的實現,叫
graphql.schema.visibility.BlockedFields..

GraphqlFieldVisibility blockedFields = BlockedFields.newBlock()
        .addPattern("Character.id")
        .addPattern("Droid.appearsIn")
        .addPattern(".*\\.hero") // it uses regular expressions
        .build();

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(StarWarsSchema.queryType)
        .fieldVisibility(blockedFields)
        .build();

若是你須要,還有一個實現能夠防止 instrumentation 攔截你的 schema。

請注意,這會使您的服務器違反graphql規範和大多數客戶端的預期,所以請謹慎使用.

GraphQLSchema schema = GraphQLSchema.newSchema()
        .query(StarWarsSchema.queryType)
        .fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)
        .build();

你能夠編寫本身的 GraphqlFieldVisibility 來控制字段的可見性。

class CustomFieldVisibility implements GraphqlFieldVisibility {

    final YourUserAccessService userAccessService;

    CustomFieldVisibility(YourUserAccessService userAccessService) {
        this.userAccessService = userAccessService;
    }

    @Override
    public List<GraphQLFieldDefinition> getFieldDefinitions(GraphQLFieldsContainer fieldsContainer) {
        if ("AdminType".equals(fieldsContainer.getName())) {
            if (!userAccessService.isAdminUser()) {
                return Collections.emptyList();
            }
        }
        return fieldsContainer.getFieldDefinitions();
    }

    @Override
    public GraphQLFieldDefinition getFieldDefinition(GraphQLFieldsContainer fieldsContainer, String fieldName) {
        if ("AdminType".equals(fieldsContainer.getName())) {
            if (!userAccessService.isAdminUser()) {
                return null;
            }
        }
        return fieldsContainer.getFieldDefinition(fieldName);
    }
}

查詢緩存(Query Caching)

Before the graphql-java engine executes a query it must be parsed and
validated, and this process can be somewhat time consuming.

爲了不重複的解釋和校驗。 GraphQL.Builder
可使用PreparsedDocumentProvider去重用 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. 建立你須要的緩存實例,本例子是使用的是 Caffeine
    。它是個高質量的緩存解決方案。緩存實例應該是線程安全和能夠線程間共享的。
  2. PreparsedDocumentProvider 是一個函式接口( functional
    interface),方法名是get。.

爲提升緩存命中率,GraphQL 語句中的 field 參數(arguments)建議使用變量(
variables)來表達,而不是直接把值寫在語句中。

下面的查詢 :

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

應該寫成:

query HelloTo($to: String!) {
     sayHello(to: $to) {
        greeting
     }
}

帶上參數( variables):

{
   "to": "Me"
}

這樣,這無論查詢的變量(variable)如何變化 ,查詢解釋也就能夠重用。

相關文章
相關標籤/搜索