graphql-java使用手册:part3 执行(Execution)

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

执行(Execution)

查询(Queries)

为了对 一个Schema 执行查询。需要先构造一个 GraphQL
对象,并带着一些参数去调用 execute() 方法.

查询将返回一个 ExecutionResult 对象,其中包含查询的结果数据
(或出错时的错误信息集合).

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 查询测试用例

Data Fetchers

每个graphql schema 中的field,都需要绑定相应的
graphql.schema.DataFetcher 以获取数据. 其它GraphQL的实现把这叫
resolvers*.

很多时候,你可以用默认的 graphql.schema.PropertyDataFetcher 去从 Java
POJO 中自动提取数据到对应的 field. 如果你未为 field 指定 data fetcher
那么就默认使用它.

但你最少需要为顶层的领域对象(domain objects) 编写 data fetchers.
其中可以会与database交互,或用HTTP与其它系统交互.

graphql-java 不关心你如何获取你的业务数据,这是你的自己.
它也不关心你如果授权你的业务数据.
你应该在自己的业务逻辑层,去实现这些逻辑.

简单 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/graphql/#sec-Response 中的规范

ExecutionResult executionResult = graphQL.execute(executionInput);

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

sendAsJson(toSpecificationResult);

更新(Mutations)

如果你不了解什么叫更新(Mutations),建议先阅读规范
http://graphql.org/learn/queries/#mutations.

首先,你需要定义一个支持输入参数的 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/graphql/#sec-Query 允许和推荐的。

详细见 规范 .

异步顺序执行策略(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)如何变化 ,查询解释也就可以重用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容