graphql-java:使用 Dataloader

使用 Dataloader

使用 graphql, 你很可能会去查询图结构的数据(graph of data ) (这可能是句废话). 如果用简单直接的方法去获取每个field的数据,可能会效率很低。

使用 java-dataloader 可以帮助你更有效地缓存和批量化数据加载操作。 ``dataloader``会缓存所有加载过的数据,使再次使用相同数据时,不需要再加载。

假设我们用以下的 StarWars 查询。这查询了一个英雄( hero)和他朋友的名字,和他朋友的朋友的名字。很多时候,他们有共同的朋友。

{
    hero {
        name
        friends {
            name
            friends {
               name
            }
        }
    }
}

下面是查询的结果。你可以看到,Han, Leia, Luke 和 R2-D2 是一群紧密的朋友。他们有很多共同的朋友。

[hero: [name: 'R2-D2', friends: [
        [name: 'Luke Skywalker', friends: [
                [name: 'Han Solo'], [name: 'Leia Organa'], [name: 'C-3PO'], [name: 'R2-D2']]],
        [name: 'Han Solo', friends: [
                [name: 'Luke Skywalker'], [name: 'Leia Organa'], [name: 'R2-D2']]],
        [name: 'Leia Organa', friends: [
                [name: 'Luke Skywalker'], [name: 'Han Solo'], [name: 'C-3PO'], [name: 'R2-D2']]]]]
]

一个直接的实现是为每个人物对象(person object)调用一次 DataFetcher 去获取数据。

这样你需要 15 次网络调用 。即使这群有有很多相同的朋友。使用 dataloader 可以让 graphql 查询更高效。

graphql 会批量化每个层级的查询。 ( 如先是 hero 之后是 friends 之后是他们的 friends), Data loader 返回了一个 ” 期约(promise)”,期约会返回一个 person object.(人物对象)。在查询的每个层级, dataloader.dispatch() 方法均会被调用一次,以获取真实的数据。当开启了缓存功能时 (默认开启),将直接返回之前加载过的 person,而不会再发起一次查询。

上例中,共有 5 个独立的 people。但当缓存和批量化开启后,只发起了 3 次调用 batch loader 方法的查询操作。3 次网络或DB访问,当然比 15 次牛B多了。【译者补】

如果你使用了如 java.util.concurrent.CompletableFuture.supplyAsync() 的异步程序方式 。多个field的远程加载数据就可以并发进行了。. 这可以让查询更快,因一次并发了多个远程调用。

下面就是示例代码:

// a batch loader function that will be called with N or more keys for batch loading
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
    @Override
    public CompletionStage<List<Object>> load(List<String> keys) {
        //
        // we use supplyAsync() of values here for maximum parellisation
        //
        return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
    }
};

// a data loader for characters that points to the character batch loader
DataLoader<String, Object> characterDataLoader = new DataLoader<>(characterBatchLoader);

//
// use this data loader in the data fetchers associated with characters and put them into
// the graphql schema (not shown)
//
DataFetcher heroDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        return characterDataLoader.load("2001"); // R2D2
    }
};

DataFetcher friendsDataFetcher = new DataFetcher() {
    @Override
    public Object get(DataFetchingEnvironment environment) {
        StarWarsCharacter starWarsCharacter = environment.getSource();
        List<String> friendIds = starWarsCharacter.getFriendIds();
        return characterDataLoader.loadMany(friendIds);
    }
};

//
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
// in this case there is 1 but you can have many
//
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("character", characterDataLoader);

//
// this instrumentation implementation will dispatch all the dataloaders
// as each level fo the graphql query is executed and hence make batched objects
// available to the query and the associated DataFetchers
//
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
        = new DataLoaderDispatcherInstrumentation(registry);

//
// now build your graphql object and execute queries on it.
// the data loader will be invoked via the data fetchers on the
// schema fields
//
GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
        .instrumentation(dispatcherInstrumentation)
        .build();

```

需要注意的是,只有你使用了 DataLoaderDispatcherInstrumentation ,上面说的才会生效。由它来调用 dataLoader.dispatch() 。不然,期约( promises ) 将不会被执行,就更不会有数据获取了。

查询范围的 Data Loaders

对于 Web 请求,请求的结果可能会因不同用户而不同的。如果是特定用户的数据,你一定不希望用户A的数据,被用户B查询到。

所以 DataLoader 实例的范围很重要。这时,你需要对每个 Request 创建一个新的 DataLoader,来保证它只在当前请求中生效。

如果你需要的是不同请求间共享数据,所以你会希望 DataLoader 的生命周期更长。

但如用你用请求级的 data loaders ,为每个请求创建 GraphQL and DataLoader 是花费很少资源的。Its the GraphQLSchema creation that can be expensive, especially if you are using graphql SDL parsing.

i在代码中静态引用 schema 。可以是静态变量或 IoC 单件组件。但每次处理请求时,都需要创建 GraphQL 对象。

GraphQLSchema staticSchema = staticSchema_Or_MayBeFrom_IoC_Injection();

DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("character", getCharacterDataLoader());

DataLoaderDispatcherInstrumentation dispatcherInstrumentation
        = new DataLoaderDispatcherInstrumentation(registry);

GraphQL graphQL = GraphQL.newGraphQL(staticSchema)
        .instrumentation(dispatcherInstrumentation)
        .build();

graphQL.execute("{ helloworld }");

// you can now throw away the GraphQL and hence DataLoaderDispatcherInstrumentation
// and DataLoaderRegistry objects since they are really cheap to build per request

graphql-java:运行期异常(Runtime Exceptions )

运行期异常(Runtime Exceptions )

在一些异常的情况下, graphql engine 有可能抛出 Runtime exceptions。下面是调用 graphql.execute(...) 期间可能出现的异常。

他们不是执行 graphql 查询中发生的异常。 但还是不能被忽略。

  • graphql.schema.CoercingSerializeException

发生这类型的异常,原因是序列化Scalar时出错。如 Int 字段获取到 String 值。

  • graphql.schema.CoercingParseValueException

发生这类型的异常,原因是解释输入的 Scalar 时出错 ,如 int 参数的实际输入值是一个 String。

  • graphql.execution.UnresolvedTypeException

当 graphql.schema.TypeResolver` 不能判断抽象对象( interface or union type) 的实际类型( concrete object type) 时发生这个异常。

  • graphql.execution.NonNullableValueCoercedAsNullException

如果 个不允许为 null 的参数,被赋值为 null。会抛出上面异常。.

  • graphql.execution.InputMapDefinesTooManyFieldsException

可以输入对象( input type object )包含了未在Schema中定义的field。就会发生上面异常。

  • graphql.schema.validation.InvalidSchemaException
表示在运行下面函数时, Schema 校验失败。
graphql.schema.GraphQLSchema.Builder#build()`
  • graphql.GraphQLException

这个是通用的异常。如不能访问 POJO 的 field。这可以等同于 RuntimeException。

  • graphql.AssertException

这是内部断言的预计外异常

事实上不应该发生【译注:如果抛出了,可能是框架有BUG了。】

graphql-java:订阅(Subscriptions)

订阅(Subscriptions)

订阅查询(Subscription Queries)

Graphql 订阅(subscriptions)使你可以让你订阅响应式数据源(reactive source) 。当有新数据时,会发送给订阅者。

可以阅读 http://graphql.org/blog/subscriptions-in-graphql-and-relay/ 来了解订阅的背景知识。

假设你有一个股票服务。可以用这个 graphql 语句来订阅它的数据:

subscription StockCodeSubscription {
    stockQuotes(stockCode:"IBM') {
        dateTime
        stockCode
        stockPrice
        stockPriceChange
    }
}

股票价格变化时,graphql 订阅 可以把 ExecutionResult 对象以流的方式传送给订阅者。和其它 graphql 查询一样,只会发送指定的字段 。

不同的是,一开始的查询结果是一个响应式流(reactive-streams) Publisher(流发布者) 对象。通过对象可以获取未来的数据。

你需要使用 SubscriptionExecutionStrategy 策略作为执行策略(execution strategy)。因为它支持 reactive-streams APIs.

GraphQL graphQL = GraphQL
        .newGraphQL(schema)
        .subscriptionExecutionStrategy(new SubscriptionExecutionStrategy())
        .build();

ExecutionResult executionResult = graphQL.execute(query);

Publisher<ExecutionResult> stockPriceStream = executionResult.getData();

这里的 Publisher<ExecutionResult> 就是流事件的发布者【译注:原文 publisher of a stream of events】。你需要编写你自己的流处理代码,如:

GraphQL graphQL = GraphQL
        .newGraphQL(schema)
        .subscriptionExecutionStrategy(new SubscriptionExecutionStrategy())
        .build();

String query = "" +
        "    subscription StockCodeSubscription {\n" +
        "        stockQuotes(stockCode:\"IBM') {\n" +
        "            dateTime\n" +
        "            stockCode\n" +
        "            stockPrice\n" +
        "            stockPriceChange\n" +
        "        }\n" +
        "    }\n";

ExecutionResult executionResult = graphQL.execute(query);

Publisher<ExecutionResult> stockPriceStream = executionResult.getData();

AtomicReference<Subscription> subscriptionRef = new AtomicReference<>();
stockPriceStream.subscribe(new Subscriber<ExecutionResult>() {

    @Override
    public void onSubscribe(Subscription s) {
        subscriptionRef.set(s);
        s.request(1);
    }

    @Override
    public void onNext(ExecutionResult er) {
        //
        // process the next stock price
        //
        processStockPriceChange(er.getData());

        //
        // ask the publisher for one more item please
        //
        subscriptionRef.get().request(1);
    }

    @Override
    public void onError(Throwable t) {
        //
        // The upstream publishing data source has encountered an error
        // and the subscription is now terminated.  Real production code needs
        // to decide on a error handling strategy.
        //
    }

    @Override
    public void onComplete() {
        //
        // the subscription has completed.  There is not more data
        //
    }
});

需要编写 reactive-streams 代码去消费一源源不断的 ExecutionResults。你可以在 http://www.reactive-streams.org/ 中看到更 reactive-streams 代码的编写细节。

``RxJava``是这个流行的 reactive-streams 实现。在 http://reactivex.io/intro.html 中可以看到更多创建Publishers 数据 和 Subscriptions 数据的细节。

graphql-java 只是产出一个流对象。它不关心如何在网络上用 web sockets 或其它手段发送流数据 。虽然这很重要,但不是作为基础 graphql-java 库应该做的。

我们编写了一个 websockets 的(基于 Jetty) 模拟股票报价的示例应用。它使用了 RxJava。

详见 https://github.com/graphql-java/graphql-java-subscription-example

关于订阅服务的 Data Fetchers

订阅字段的 DataFetcher 的职责是生成一个 Publisher。这个 Publisher 输出的每一个对象,将会通过 graphql 查询来映射。然后作为执行结果返回。

你会像这样子去编写Data Fetcher:

DataFetcher<Publisher<StockInfo>> publisherDataFetcher = new DataFetcher<Publisher<StockInfo>>() {
    @Override
    public Publisher<StockInfo> get(DataFetchingEnvironment environment) {
        String stockCodeArg = environment.getArgument("stockCode");
        return buildPublisherForStockCode(stockCodeArg);
    }
};

如何获取流事件,就由你的 reactive code 来决定 了。graphql-java 会帮助你从流对象中获取 graphql 字段(fields)。像一般的 graphql 查询一样。

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

graphql-java:创建Schema

创建Schema

Schema的主要用途是定义所有可供查询的字段(field),它们最终组合成一套完整的GraphQL API.

“graphql-java”提供两种方法来定义Schema。用java代码来定义、用GraphQL SDL(即IDL)来定义。

注意:SDL(IDL)现在还不是 官方 graphql 规范. 本GraphQL实现,是基于 已有的JS参考实现 来开发的。但JS参考实现中的很多代码也是基于SDL(IDL)语法的,所以你可以认为这语法是可以长期使用的.

如果你不确认用“java代码”还是用“GraphQL SDL(即IDL)”来定义你的Schema,那么我们建议你用SDL(IDL)

SDL example:

type Foo {
    bar: String
}

java代码例子:

GraphQLObjectType fooType = newObject()
    .name("Foo")
    .field(newFieldDefinition()
            .name("bar")
            .type(GraphQLString))
    .build();

DataFetcher 与 TypeResolver

对象 DataFetcher 作用是获取字段(field)对应的数据;另外,在修改(mutation)操作时,可以更新数据

每个字段都有自己的 DataFetcher. 如果未为字段指定DataFetcher, 那么自动使用默认的 PropertyDataFetcher .

PropertyDataFetcherMap 和 Java Beans 中获取数据. 所以,当Schema中的field名,与Map中的key值,或 Source Object 中的 java bean 字段名相同时,不需要为field指定 DataFetcher.

对象 TypeResolver 帮助 graphql-java 判断数据的实际类型(type). 所以 InterfaceUnion 均需要指定关联的 TypeResolver(类型识别器) .

例如,你有一个 InterfaceMagicUserType 它有可能是以下的具体类型(Type) Wizard, Witch and Necromancer. Type resolver(类型识别器) 的作用是在运行时识别出 GraphqlObjectType 的具体类型(Type)。后期具体类型下的field相关的 data fetcher被调用并获取数据.

new TypeResolver() {
    @Override
    public GraphQLObjectType getType(TypeResolutionEnvironment env) {
        Object javaObject = env.getObject();
        if (javaObject instanceof Wizard) {
            return (GraphQLObjectType) env.getSchema().getType("WizardType");
        } else if (javaObject instanceof Witch) {
            return (GraphQLObjectType) env.getSchema().getType("WitchType");
        } else {
            return (GraphQLObjectType) env.getSchema().getType("NecromancerType");
        }
    }
};

用 SDL 创建 Schema

当使用SDL方法来开发时,你需要同时编写对应的 DataFetcherTypeResolver

很大的 Schema IDL 文件很难查看。

schema {
    query: QueryType
}

type QueryType {
    hero(episode: Episode): Character
    human(id : String) : Human
    droid(id: ID!): Droid
}


enum Episode {
    NEWHOPE
    EMPIRE
    JEDI
}

interface Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
}

type Human implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    homePlanet: String
}

type Droid implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    primaryFunction: String
}

由于Schema中只是指定了静态的字段和类型,你还需要把它绑定到java方法中。以让Schema可以运行起来

这里的绑定,包括 DataFetcher , TypeResolvers 与自定义 Scalar.

用下页的Builder方法,就可以绑定Schema和Java程序

RuntimeWiring buildRuntimeWiring() {
    return RuntimeWiring.newRuntimeWiring()
            .scalar(CustomScalar)
            // this uses builder function lambda syntax
            .type("QueryType", typeWiring -> typeWiring
                    .dataFetcher("hero", new StaticDataFetcher(StarWarsData.getArtoo()))
                    .dataFetcher("human", StarWarsData.getHumanDataFetcher())
                    .dataFetcher("droid", StarWarsData.getDroidDataFetcher())
            )
            .type("Human", typeWiring -> typeWiring
                    .dataFetcher("friends", StarWarsData.getFriendsDataFetcher())
            )
            // you can use builder syntax if you don't like the lambda syntax
            .type("Droid", typeWiring -> typeWiring
                    .dataFetcher("friends", StarWarsData.getFriendsDataFetcher())
            )
            // or full builder syntax if that takes your fancy
            .type(
                    newTypeWiring("Character")
                            .typeResolver(StarWarsData.getCharacterTypeResolver())
                            .build()
            )
            .build();
}

最后,你可以通过整合静态 Schema 和 绑定(wiring),而生成一个可以执行的 Schema。

SchemaParser schemaParser = new SchemaParser();
SchemaGenerator schemaGenerator = new SchemaGenerator();

File schemaFile = loadSchema("starWarsSchema.graphqls");

TypeDefinitionRegistry typeRegistry = schemaParser.parse(schemaFile);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, wiring);

除了上面的 builder 风格, TypeResolver s 与 DataFetcher s 也可以通过 WiringFactory 接口绑定在一起。通过程序去分析 SDL ,就可以允许更自由的绑定。你可以 通过分析 SDL 声明, 或其它 SDL 定义去决定你的运行时逻辑。

RuntimeWiring buildDynamicRuntimeWiring() {
    WiringFactory dynamicWiringFactory = new WiringFactory() {
        @Override
        public boolean providesTypeResolver(TypeDefinitionRegistry registry, InterfaceTypeDefinition definition) {
            return getDirective(definition,"specialMarker") != null;
        }

        @Override
        public boolean providesTypeResolver(TypeDefinitionRegistry registry, UnionTypeDefinition definition) {
            return getDirective(definition,"specialMarker") != null;
        }

        @Override
        public TypeResolver getTypeResolver(TypeDefinitionRegistry registry, InterfaceTypeDefinition definition) {
            Directive directive  = getDirective(definition,"specialMarker");
            return createTypeResolver(definition,directive);
        }

        @Override
        public TypeResolver getTypeResolver(TypeDefinitionRegistry registry, UnionTypeDefinition definition) {
            Directive directive  = getDirective(definition,"specialMarker");
            return createTypeResolver(definition,directive);
        }

        @Override
        public boolean providesDataFetcher(TypeDefinitionRegistry registry, FieldDefinition definition) {
            return getDirective(definition,"dataFetcher") != null;
        }

        @Override
        public DataFetcher getDataFetcher(TypeDefinitionRegistry registry, FieldDefinition definition) {
            Directive directive = getDirective(definition, "dataFetcher");
            return createDataFetcher(definition,directive);
        }
    };
    return RuntimeWiring.newRuntimeWiring()
            .wiringFactory(dynamicWiringFactory).build();
}

用代码方式创建 schema

如果用程序方式来定义 Schema,在创建类型(type)的时候,你需要提供 DataFetcher and ``TypeResolver` 。

如:

DataFetcher<Foo> fooDataFetcher = environment -> {
        // environment.getSource() is the value of the surrounding
        // object. In this case described by objectType
        Foo value = perhapsFromDatabase(); // Perhaps getting from a DB or whatever
        return value;
}

GraphQLObjectType objectType = newObject()
        .name("ObjectType")
        .field(newFieldDefinition()
                .name("foo")
                .type(GraphQLString)
                .dataFetcher(fooDataFetcher))
        .build();

类型(Types)

GraphQL 类型系统支持以下类型

  • Scalar
  • Object
  • Interface
  • Union
  • InputObject
  • Enum

Scalar

graphql-java 支持以下基本数据类型( Scalars)。

  • GraphQLString
  • GraphQLBoolean
  • GraphQLInt
  • GraphQLFloat
  • GraphQLID
  • GraphQLLong
  • GraphQLShort
  • GraphQLByte
  • GraphQLFloat
  • GraphQLBigDecimal
  • GraphQLBigInteger

Object

SDL Example:

type SimpsonCharacter {
    name: String
    mainCharacter: Boolean
}

Java 例子:

GraphQLObjectType simpsonCharacter = newObject()
.name("SimpsonCharacter")
.description("A Simpson character")
.field(newFieldDefinition()
        .name("name")
        .description("The name of the character.")
        .type(GraphQLString))
.field(newFieldDefinition()
        .name("mainCharacter")
        .description("One of the main Simpson characters?")
        .type(GraphQLBoolean))
.build();

Interface

Interfaces are abstract definitions of types.

SDL Example:

interface ComicCharacter {
    name: String;
}

Java 例子:

GraphQLInterfaceType comicCharacter = newInterface()
    .name("ComicCharacter")
    .description("An abstract comic character.")
    .field(newFieldDefinition()
            .name("name")
            .description("The name of the character.")
            .type(GraphQLString))
    .build();

Union

SDL Example:

interface Cat {
    name: String;
    lives: Int;
}

interface Dog {
    name: String;
    bonesOwned: int;
}

union Pet = Cat | Dog

Java 例子:

GraphQLUnionType PetType = newUnionType()
    .name("Pet")
    .possibleType(CatType)
    .possibleType(DogType)
    .typeResolver(new TypeResolver() {
        @Override
        public GraphQLObjectType getType(TypeResolutionEnvironment env) {
            if (env.getObject() instanceof Cat) {
                return CatType;
            }
            if (env.getObject() instanceof Dog) {
                return DogType;
            }
            return null;
        }
    })
    .build();

Enum

SDL Example:

enum Color {
    RED
    GREEN
    BLUE
}

Java 例子:

GraphQLEnumType colorEnum = newEnum()
    .name("Color")
    .description("Supported colors.")
    .value("RED")
    .value("GREEN")
    .value("BLUE")
    .build();

ObjectInputType

SDL Example:

input Character {
    name: String
}

Java 例子:

GraphQLInputObjectType inputObjectType = newInputObject()
    .name("inputObjectType")
    .field(newInputObjectField()
            .name("field")
            .type(GraphQLString))
    .build();

类型引用 (Type References) (递归类型recursive types)

GraphQL 支持递归类型:如 Person(人) 可以包含很多朋友【译注:当然这些也是人类型的】

为了方便声明这种情况, graphql-java 有一个 GraphQLTypeReference 类。

在实际的 Schema 创建时,GraphQLTypeReference 会变为实际的类型。

例如:

GraphQLObjectType person = newObject()
    .name("Person")
    .field(newFieldDefinition()
            .name("friends")
            .type(new GraphQLList(new GraphQLTypeReference("Person"))))
    .build();

如果用SDL(ID L)来定义 Schema ,不需要特殊的处理。

Schema IDL的模块化

很大的 Schema IDL 文件很难查看。所以我们有两种方法可以模块化 Schema。

方法一是合并多个 Schema IDL 文件到一个逻辑单元( logic unit)。下面的例子是,在 Schema 生成前,合并多个独立的文件。

SchemaParser schemaParser = new SchemaParser();
SchemaGenerator schemaGenerator = new SchemaGenerator();

File schemaFile1 = loadSchema("starWarsSchemaPart1.graphqls");
File schemaFile2 = loadSchema("starWarsSchemaPart2.graphqls");
File schemaFile3 = loadSchema("starWarsSchemaPart3.graphqls");

TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();

// each registry is merged into the main registry
typeRegistry.merge(schemaParser.parse(schemaFile1));
typeRegistry.merge(schemaParser.parse(schemaFile2));
typeRegistry.merge(schemaParser.parse(schemaFile3));

GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, buildRuntimeWiring());

Graphql IDL 还有其它方法去做模块化。你可以使用 type extensions 去为现有类型增加字段和 interface。

例如,一开始,你有这样一个文件:

type Human {
    id: ID!
    name: String!
}

你的系统的其它模块可以扩展这个类型:

extend type Human implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
}

你可以按你的需要去扩展。它们会以被发现的顺序组合起来。重复的字段会被合并(但重定义一个字段的类型是不允许的)。

extend type Human {
    homePlanet: String
}

完成合并后的 Human 类型会是这样的:

type Human implements Character {
    id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
    homePlanet: String
}

这在顶层查询中特别有用。你可以用 extension types 去为顶层 “query” 增加字段每个团队可以提供自己的字段集,进而合成完整的查询。

schema {
  query: CombinedQueryFromMultipleTeams
}

type CombinedQueryFromMultipleTeams {
    createdTimestamp: String
}

# maybe the invoicing system team puts in this set of attributes
extend type CombinedQueryFromMultipleTeams {
    invoicing: Invoicing
}

# and the billing system team puts in this set of attributes
extend type CombinedQueryFromMultipleTeams {
    billing: Billing
}

# and so and so forth
extend type CombinedQueryFromMultipleTeams {
    auditing: Auditing
}

Subscription(订阅)的支持

订阅功能还未在规范中: graphql-java 现在只支持简单的实现 ,你可以用 GraphQLSchema.Builder.subscription(...) 在 Schema 中定义订阅。这使你可以处理订阅请求。

subscription foo {
    # normal graphql query
}

graphql-java:入门

入门

graphql-java 需要运行于 Java 8 或更高版本.

如何在 Gradle 中使用最新正式版本

首先,保证 mavenCentral 在你的 repos 库列表中:

repositories {
    mavenCentral()
}

依赖:

dependencies {
  compile 'com.graphql-java:graphql-java:6.0'
}

如果在 Maven 中使用最新正式版本

依赖:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>6.0</version>
</dependency>

Hello World【译注:这个用不翻译了吧 🙂 】

下面就用 graphql-java 来实现经典的 “hello world” :

import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.StaticDataFetcher;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring;

public class HelloWorld {

    public static void main(String[] args) {
        String schema = "type Query{hello: String}";

        SchemaParser schemaParser = new SchemaParser();
        TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema);

        RuntimeWiring runtimeWiring = newRuntimeWiring()
                .type("Query", builder -> builder.dataFetcher("hello", new StaticDataFetcher("world")))
                .build();

        SchemaGenerator schemaGenerator = new SchemaGenerator();
        GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);

        GraphQL build = GraphQL.newGraphQL(graphQLSchema).build();
        ExecutionResult executionResult = build.execute("{hello}");

        System.out.println(executionResult.getData().toString());
        // Prints: {hello=world}
    }
}

如何使用最新的开发中版本

最近的开发中版本,可以在 Bintray 上获取.

可以从 最新版本 中看到最新的版本号.

如果在 Gradle 中使用最新的开发中版本

增加 repositories:

repositories {
    mavenCentral()
    maven { url  "http://dl.bintray.com/andimarek/graphql-java" }
}

依赖:

dependencies {
  compile 'com.graphql-java:graphql-java:INSERT_LATEST_VERSION_HERE'
}

如果在 Maven 中使用最新的开发中版本

增加 repository:

<repository>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
    <id>bintray-andimarek-graphql-java</id>
    <name>bintray</name>
    <url>http://dl.bintray.com/andimarek/graphql-java</url>
</repository>

依赖:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>INSERT_LATEST_VERSION_HERE</version>
</dependency>

GraphQL-Java 使用文档翻译之路

步骤

1.检查工作git工作目录版本

2.修改conf.py文件

language = 'zh_CN' # language supported
locale_dirs = ['locale/']   # path is example but recommended.
gettext_compact = False     # optional.

3.生成POT文件

$ make gettext

生成了_build/gettext目录,和其中的 POT 文件。

4.生成 PO 文件

$ sphinx-intl update -p _build/gettext -l zh_CN -l en

输出文件locale/zh_CN/LC_MESSAGES/*.po

5.翻译./locale//LC_MESSAGES/ 中的PO文件
建议为工具:OmegaT

6.生成html

$ make -e SPHINXOPTS="-D language='zh_CN'" html

参考

什么是rst文件

http://docutils.sourceforge.net/rst.html

翻译rst文件的正确操作

http://www.sphinx-doc.org/en/stable/intl.html

还是这个中文入门好看好懂:
使用Sphinx翻译LLVM的中文文档

太吓人的流程