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

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

執(zhí)行(Execution)

查詢(Queries)

為了對 一個Schema 執(zhí)行查詢。需要先構(gòu)造一個 GraphQL
對象,并帶著一些參數(shù)去調(diào)用 execute() 方法.

查詢將返回一個 ExecutionResult 對象,其中包含查詢的結(jié)果數(shù)據(jù)
(或出錯時的錯誤信息集合).

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

更復(fù)雜的示例,可以看 StarWars 查詢測試用例

Data Fetchers

每個graphql schema 中的field,都需要綁定相應(yīng)的
graphql.schema.DataFetcher 以獲取數(shù)據(jù). 其它GraphQL的實現(xiàn)把這叫
resolvers*.

很多時候,你可以用默認(rèn)的 graphql.schema.PropertyDataFetcher 去從 Java
POJO 中自動提取數(shù)據(jù)到對應(yīng)的 field. 如果你未為 field 指定 data fetcher
那么就默認(rèn)使用它.

但你最少需要為頂層的領(lǐng)域?qū)ο?domain objects) 編寫 data fetchers.
其中可以會與database交互,或用HTTP與其它系統(tǒng)交互.

graphql-java 不關(guān)心你如何獲取你的業(yè)務(wù)數(shù)據(jù),這是你的自己.
它也不關(guān)心你如果授權(quán)你的業(yè)務(wù)數(shù)據(jù).
你應(yīng)該在自己的業(yè)務(wù)邏輯層,去實現(xiàn)這些邏輯.

簡單 Data fetcher 示例:

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

框架在執(zhí)行查詢時。會調(diào)用上面的方法,其中的
graphql.schema.DataFetchingEnvironment 參數(shù)包括以下信息:被查詢的
field、查詢這個field時可能帶上的查詢參數(shù)、這個field的父數(shù)據(jù)對象(Source
Object)、 查詢的ROOT數(shù)據(jù)對象、查詢執(zhí)行上下文環(huán)境對象(query context
object).

上面是同步獲取數(shù)據(jù)的例子,執(zhí)行引擎需要等待一個 data fetcher
返回數(shù)據(jù)才能繼續(xù)下一個. 也可以通過編寫異步的 DataFetcher ,異步地返回
CompletionStage 對象,在下文中將會說明使用方法.

當(dāng)獲取數(shù)據(jù)出現(xiàn)異常時

如果異步是出現(xiàn)在調(diào)用 data fetcher 時, 默認(rèn)的執(zhí)行策略(execution strategy)
將生成一個 graphql.ExceptionWhileDataFetching
錯誤,并將其加入到查詢結(jié)果的錯誤列表中. 請留意,GraphQL
在發(fā)生異常時,允許返回部分成功的數(shù)據(jù),并將帶上異常信息.

下面是默認(rèn)的異常行為處理邏輯.

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)轉(zhuǎn)換到
ExceptionWhileDataFetching 對象中.
這可以方便你把自己的錯誤信息,放到返回給調(diào)用者的 GraphQL 錯誤列表中.

例如,你在 DataFetcher 中拋出了這個異常. 那么 foo and fizz
屬性將會包含在返回給調(diào)用者的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
來改變這些邏輯。只需要在執(zhí)行策略(execution strategy)注冊一下.

例如,上面的代碼記錄了底層的異常和堆棧.
如果你不希望這些出現(xiàn)在輸出的錯誤列表中。你可以用以下的方法去實現(xiàn).

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 方法去調(diào)用 graphql ,用 JSON 格式作為返回結(jié)果.
返回,需要把 graphql.ExecutionResult 對象轉(zhuǎn)換為 JSON 格式包.

一般用 Jackson or GSON 去做 JSON 序列化.
但他們對結(jié)果數(shù)據(jù)的轉(zhuǎn)換方法有一些不同點. 例如 JSON 的`nulls` 在 graphql
結(jié)果中的是有用的。所以必須在 json mappers 中設(shè)置需要它

為保證你返回的 JSON 結(jié)果 100% 合符 graphql 規(guī)范, 應(yīng)該調(diào)用result對象的
toSpecification 方法,然后以 JSON格式 發(fā)送響應(yīng).

這樣就可以確保返回數(shù)據(jù)合符在
http://facebook.github.io/graphql/#sec-Response 中的規(guī)范

ExecutionResult executionResult = graphQL.execute(executionInput);

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

sendAsJson(toSpecificationResult);

更新(Mutations)

如果你不了解什么叫更新(Mutations),建議先閱讀規(guī)范
http://graphql.org/learn/queries/#mutations.

首先,你需要定義一個支持輸入?yún)?shù)的 GraphQLObjectType .
在更新數(shù)據(jù)時,框架會帶上這些參數(shù)去調(diào)用 data fetcher.

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

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

修改操作是需要帶輸入?yún)?shù)的,上例中對應(yīng)變量 $ep and $review

對應(yīng)地,Schema 應(yīng)該這么寫【譯注:以下是 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();

注意,輸入?yún)?shù)應(yīng)該是 GraphQLInputObjectType 類型. 請留意.
對于修改操作,輸入?yún)?shù)只能用這個類型(type),而不能用如
>><<GraphQLObjectType之類的輸出類型(type). Scalars 類型(type)
可以用于輸入和輸出.

對于更新操作,DataFetcher的職責(zé)是執(zhí)行數(shù)據(jù)更新行返回執(zhí)行結(jié)果.

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

上面代碼,先更新業(yè)務(wù)數(shù)據(jù),然后返回 Review 對象給調(diào)用方.

異步執(zhí)行(Asynchronous Execution)

graphql-java 是個全異步的執(zhí)行引擎. 如下,調(diào)用 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)或函數(shù)你的函數(shù).
需要你需要同步等待執(zhí)行結(jié)果 ,可以調(diào)用 .join() 方法.

graphql-java引擎內(nèi)部是異步執(zhí)行的,但你可以通過調(diào)用 join
方法變?yōu)橥降却? 下面是等效的代碼:

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>
對象,那么它會被糅合到整個異步查詢中.
這樣,你可以同時發(fā)起我個數(shù)據(jù)獲取操作,讓它們并行運行.
而由DataFetcher控制具體的線程并發(fā)策略.

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

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
規(guī)范的執(zhí)行結(jié)果.

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

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

關(guān)于執(zhí)行策略(Execution Strategies)

在執(zhí)行查詢或更新數(shù)據(jù)時,引擎會使用實現(xiàn)了
>><<graphql.execution.ExecutionStrategy接口 的對象,來決定執(zhí)行策略.
graphql-java 中已經(jīng)有幾個現(xiàn)成的策略,但如果你需要,你可以寫自己的。.

你可以這樣給 GraphQL 對象綁定執(zhí)行策略。

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

實際上,上面就是引擎默認(rèn)的策略了。大部分情況下用它就夠了。

異步執(zhí)行策略(AsyncExecutionStrategy)

默認(rèn)的查詢 執(zhí)行策略是 graphql.execution.AsyncExecutionStrategy
,它會把每個 field 返回視為 CompleteableFuture 。它并不會控制 filed
的獲取順序. 這個策略可以優(yōu)化查詢執(zhí)行的性能.

Data fetchers 返回 CompletionStage`
對象,就可以全異步執(zhí)行整個查詢了。

例如以下的查詢:

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.

這個策略不會按順序來集成結(jié)果數(shù)據(jù)。但查詢結(jié)果會按GraphQL規(guī)范順序來返回。只是數(shù)據(jù)獲取的順序不確定。

對于查詢,這個策略是 graphql 規(guī)范
http://facebook.github.io/graphql/#sec-Query 允許和推薦的。

詳細(xì)見 規(guī)范 .

異步順序執(zhí)行策略(AsyncSerialExecutionStrategy)

Graphql 規(guī)范指出,修改操作(mutations)“必須”按照 field 的順序來執(zhí)行。

所以,為了確保一個 field 一個 field
順序地執(zhí)行更新,更新操作(mutations)默認(rèn)使用
graphql.execution.AsyncSerialExecutionStrategy 策略。你的 mutation
Data Fetcher 仍然可以返回 CompletionStage 對象, 但它和其它 field
的是串行執(zhí)行的。

基于執(zhí)行器的執(zhí)行策略: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();

訂閱執(zhí)行策略(SubscriptionExecutionStrategy)

Graphql 訂閱(subscriptions) 使你可以對GraphQL
數(shù)據(jù)進(jìn)行為狀態(tài)的訂閱。你可以使用 SubscriptionExecutionStrategy
執(zhí)行策略,它支持 reactive-streams APIs。

閱讀 http://www.reactive-streams.org/ 可以得到關(guān)于 Publisher
Subscriber 接口的更多信息。

也可以閱讀subscriptions的文檔,以了解如何編寫基于支持訂閱的 graphql
服務(wù)。

批量化執(zhí)行器(BatchedExecutionStrategy)

對于有數(shù)組(list)field 的 schemas, 我們提供了
graphql.execution.batched.BatchedExecutionStrategy
策略。它可以批量化地調(diào)用標(biāo)注了@Batched 的 DataFetchers 的 get() 方法。

關(guān)于 BatchedExecutionStrategy
是如何工作的。它是如此的特別,讓我不知道如何解釋【譯注:原文:Its a
pretty special case that I don’t know how to explain properly】

控制字段的可見性

所有 GraphqlSchema
的字段(field)默認(rèn)都是可以訪問的。但有時候,你可能想不同用戶看到不同部分的字段。

你可以在schema 上綁定一個
graphql.schema.visibility.GraphqlFieldVisibility 對象。.

框架提供了一個可以指定字段(field)名的實現(xiàn),叫
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();

如果你需要,還有一個實現(xiàn)可以防止 instrumentation 攔截你的 schema。

請注意,這會使您的服務(wù)器違反graphql規(guī)范和大多數(shù)客戶端的預(yù)期,因此請謹(jǐn)慎使用.

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.

為了避免重復(fù)的解釋和校驗。 GraphQL.Builder
可以使用PreparsedDocumentProvider去重用 Document 實例。

它不是緩存 查詢結(jié)果,只是緩存解釋過的文檔( Document )。

Cache<String, PreparsedDocumentEntry> cache = Caffeine.newBuilder().maximumSize(10_000).build(); (1)
GraphQL graphQL = GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
        .preparsedDocumentProvider(cache::get) (2)
        .build();
  1. 創(chuàng)建你需要的緩存實例,本例子是使用的是 Caffeine
    。它是個高質(zhì)量的緩存解決方案。緩存實例應(yīng)該是線程安全和可以線程間共享的。
  2. PreparsedDocumentProvider 是一個函式接口( functional
    interface),方法名是get。.

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

下面的查詢 :

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

應(yīng)該寫成:

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

帶上參數(shù)( variables):

{
   "to": "Me"
}

這樣,這不管查詢的變量(variable)如何變化 ,查詢解釋也就可以重用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,627評論 18 399
  • 作者: 一字馬胡 轉(zhuǎn)載標(biāo)志 【2017-11-13】 更新日志 導(dǎo)入 作為一種強大的DSQL,學(xué)習(xí)GraphQL...
    一字馬胡閱讀 11,168評論 0 13
  • 原文:http://blog.mygraphql.com/wordpress/?p=100 創(chuàng)建Schema Sc...
    MarkZhu閱讀 2,381評論 1 1
  • 寫作應(yīng)該是一件隨時隨地的事 有一種病大概就叫強迫癥。寫東西總是想要找特定的時間,在特定的空間,有特定的字?jǐn)?shù)和自...
    安倩倩穎閱讀 346評論 0 0

友情鏈接更多精彩內(nèi)容