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)如何变化 ,查询解释也就能够重用。

相关文章
相关标签/搜索