graphql-java:执行(Execution)

执行(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/ 可以得到关于 PublisherSubscriber 接口的更多信息。

也可以阅读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)如何变化 ,查询解释也就可以重用。

Leave a Reply

Your email address will not be published. Required fields are marked *