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

Leave a Reply

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