Skip to content

Commit 37bdaa1

Browse files
authored
Generate DataLoaders on every request (#1040)
With the introduction of the new KotlinDataLoader interface we made the way to get the data loaders a property. However taking the advice from graphql-java we should generate each DataLoader every request. The factory generates the registry for every request currently but the data loaders themselves would still be static. This also updates the docs and examples around data loaders from the last PR
1 parent 53dfd61 commit 37bdaa1

File tree

20 files changed

+267
-136
lines changed

20 files changed

+267
-136
lines changed

docs/server/data-loader-registry-factory.md

Lines changed: 0 additions & 56 deletions
This file was deleted.

docs/server/data-loaders.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
id: data-loaders
3+
title: Data Loaders
4+
---
5+
6+
Data Loaders are a popular caching pattern from the [JavaScript GraphQL implementation](https://github.com/graphql/dataloader).
7+
`graphql-java` provides [support for this pattern](https://www.graphql-java.com/documentation/v16/batching/) using the `DataLoader` and `DataLoaderRegistry`.
8+
9+
Since `graphql-kotlin` allows you to abstract the schema generation and data fetching code, you may not even need data loaders if instead you have some persistant cache on your server.
10+
11+
```kotlin
12+
class User(val id: ID) {
13+
14+
// The friendService and userService, which have nothing to do with GraphQL,
15+
// should be concerned with caching and batch calls instead of your schema classes
16+
fun getFriends(): List<User> {
17+
val friends: List<ID> = friendService.getFriends(id)
18+
return userService.getUsers(friends)
19+
}
20+
21+
}
22+
```
23+
24+
If you still want to use data loaders though, they are supported through the common interfaces.
25+
26+
## `KotlinDataLoader`
27+
28+
The [GraphQLRequestHandler](./graphql-request-handler.md) accepts an optional `DataLoaderRegistryFactory` that will be used on every request.
29+
The `DataLoaderRegistryFactory` generates a new `DataLoaderRegistry` on every request. The registry is a map of a unique data loader names to a `DataLoader` object that handles the cache for an output type in your graph.
30+
A `DataLoader` caches the types by some unique value, usually by the type id, and can handle different types of batch requests.
31+
32+
To help in the registration of these various `DataLoaders`, we have created a basic interface `KotlinDataLoader`:
33+
34+
```kotlin
35+
interface KotlinDataLoader<K, V> {
36+
val dataLoaderName: String
37+
fun getDataLoader(): DataLoader<K, V>
38+
}
39+
```
40+
41+
This allows for library users to still have full control over the creation of the `DataLoader` and its various configuraiton options,
42+
but then allows common server code to handle the registration, generation on request, and execution.
43+
44+
```kotlin
45+
class UserDataLoader : KotlinDataLoader<ID, User> {
46+
override val dataLoaderName = "UserDataLoader"
47+
override fun getDataLoader() = DataLoader<ID, User>({ ids ->
48+
CompletableFuture.supplyAsync {
49+
ids.map { id -> userService.getUser(id) }
50+
}
51+
}, DataLoaderOptions.newOptions().setCachingEnabled(false))
52+
}
53+
54+
class FriendsDataLoader : KotlinDataLoader<ID, List<User>> {
55+
override val dataLoaderName = "FriendsDataLoader"
56+
override fun getDataLoader() = DataLoader<ID, List<User>> { ids ->
57+
CompletableFuture.supplyAsync {
58+
ids.map { id ->
59+
val friends: List<ID> = friendService.getFriends(id)
60+
userService.getUsers(friends)
61+
}
62+
}
63+
}
64+
}
65+
```
66+
67+
## `getValueFromDataLoader`
68+
69+
`graphql-kotlin-server` includes a helpful extension function on the `DataFetchingEnvironment` so that you can easily retrieve values from the data loaders in your schema code.
70+
71+
72+
```kotlin
73+
class User(val id: ID) {
74+
75+
@GraphQLDescription("Get the users friends using data loader")
76+
fun getFriends(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<User>> {
77+
return dataFetchingEnvironment.getValueFromDataLoader("FriendsDataLoader", id)
78+
}
79+
}
80+
```
81+
82+
83+
84+
> NOTE: Because the execution of data loaders is handled by `graphql-java`, which runs using `CompletionStage`, currently we can not support `suspend` functions when envoking data loaders.
85+
> Instead, return the `CompletableFuture` directly from the `DataLoader` response in your schema functions.
86+
> See issue [#986](https://github.com/ExpediaGroup/graphql-kotlin/issues/986).

docs/server/graphql-request-handler.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ title: GraphQLRequestHandler
44
---
55

66
The `GraphQLRequestHandler` is an open and extendable class that contains the basic logic to get a `GraphQLResponse` from `graphql-kotlin-types`.
7-
It accepts a `GraphQLRequest`, an optional [GraphQLContext](./graphql-context-factory.md) and sends that to the GraphQL schema along with the [DataLoaderRegistryFactory](./data-loader-registry-factory.md).
7+
It accepts a `GraphQLRequest`, an optional [GraphQLContext](./graphql-context-factory.md) and sends that to the GraphQL schema along with the [DataLoaderRegistry](data-loaders.md).
88

99
There shouldn't be much need to change this class but if you wanted to add custom logic or logging it is possible to override it or just create your own.

docs/server/graphql-server.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ id: graphql-server
33
title: GraphQLServer
44
---
55

6-
`graphql-kotlin-server` provides a common code and basic interfaces to setup a GraphQL server in a specific server library.
6+
`graphql-kotlin-server` provides common code and basic interfaces to setup a GraphQL server in any framework.
77

88
The official reference implementations are:
99

1010
* [graphql-kotlin-spring-server](./spring-server/spring-overview.md)
1111

12-
We reccomend using one of the implementations if you can as the common code has very little logic.
13-
It is more of a scaffolding on how to write your server code.
12+
We reccomend using one of the implementations as the common code has very little logic but you can still use the common
13+
package to create implementation for other libraries (Ktor, Spark, etc).
14+
1415
There are demos of how to use these server libraries in the `/examples` folder of the repo.
1516

1617
## `GraphQLServer`
@@ -23,7 +24,7 @@ This class is open for extensions and requires that you specify the type of the
2324

2425
In its simplest form, a GraphQL server has the following responsibilties:
2526

26-
* Parse the request info from the HTTP request
27+
* Parse the GraphQL request info from the HTTP request
2728
* Create a `GraphQLContext` object from the HTTP request to be used during execution
2829
* Send the request and the context to the GraphQL schema to execute and get a response (may contain `data` or `errors`)
2930
* Send the reponse back to the client over HTTP

docs/server/spring-server/spring-beans.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ Many of the beans are conditionally created and the default behavior can be cust
1010
| Bean | Description |
1111
|:---------------------------------|:------------|
1212
| DataFetcherExceptionHandler | GraphQL exception handler used from the various execution strategies, defaults to [KotlinDataFetcherExceptionHandler](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/exception/KotlinDataFetcherExceptionHandler.kt). |
13-
| DataLoaderRegistryFactory | Factory used to create DataLoaderRegistry instance per query execution. See [graphql-java documentation](https://www.graphql-java.com/documentation/v14/batching/) for more details. |
1413
| KotlinDataFetcherFactoryProvider | Factory used during schema construction to obtain `DataFetcherFactory` that should be used for target function (using Spring aware `SpringDataFetcher`) and property resolution. |
14+
| KotlinDataLoader (optional) | Any number of beans created that implement `KotlinDataLoader`. See [Data Loaders](../data-loaders.md) for more details. |
15+
| DataLoaderRegistryFactory | A factory class that creates a `DataLoaderRegistry` of all the `KotlinDataLoaders`. Defaults to empty registry. |
1516

1617

1718
## Non-Federated Schema

docs/server/spring-server/spring-overview.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ graphql:
5454
```
5555
5656
## Writing Schema Code
57-
In order to expose your queries, mutations and/or subscriptions in the GraphQL schema you simply need to implement
57+
In order to expose your queries, mutations, and/or subscriptions in the GraphQL schema, implement
5858
corresponding marker interface and they will be automatically picked up by `graphql-kotlin-spring-server`
5959
auto-configuration library.
6060

@@ -77,7 +77,7 @@ class MyAwesomeSubscription : Subscription {
7777
data class Widget(val id: Int, val value: String)
7878
```
7979

80-
will result in a Spring Boot reactive GraphQL web application with following schema.
80+
The above code will result in a GraphQL server with following schema:
8181

8282
```graphql
8383
schema {

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/KtorDataLoaderRegistryFactory.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616

1717
package com.expediagroup.graphql.examples.server.ktor
1818

19-
import com.expediagroup.graphql.examples.server.ktor.schema.models.BATCH_BOOK_LOADER_NAME
20-
import com.expediagroup.graphql.examples.server.ktor.schema.models.COURSE_LOADER_NAME
21-
import com.expediagroup.graphql.examples.server.ktor.schema.models.UNIVERSITY_LOADER_NAME
22-
import com.expediagroup.graphql.examples.server.ktor.schema.models.batchBookLoader
23-
import com.expediagroup.graphql.examples.server.ktor.schema.models.batchCourseLoader
24-
import com.expediagroup.graphql.examples.server.ktor.schema.models.batchUniversityLoader
19+
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.BookDataLoader
20+
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.CourseDataLoader
21+
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.UniversityDataLoader
2522
import com.expediagroup.graphql.server.execution.DataLoaderRegistryFactory
2623
import org.dataloader.DataLoaderRegistry
2724

@@ -32,9 +29,9 @@ class KtorDataLoaderRegistryFactory : DataLoaderRegistryFactory {
3229

3330
override fun generate(): DataLoaderRegistry {
3431
val registry = DataLoaderRegistry()
35-
registry.register(UNIVERSITY_LOADER_NAME, batchUniversityLoader)
36-
registry.register(COURSE_LOADER_NAME, batchCourseLoader)
37-
registry.register(BATCH_BOOK_LOADER_NAME, batchBookLoader)
32+
registry.register(UniversityDataLoader.dataLoaderName, UniversityDataLoader.getDataLoader())
33+
registry.register(CourseDataLoader.dataLoaderName, CourseDataLoader.getDataLoader())
34+
registry.register(BookDataLoader.dataLoaderName, BookDataLoader.getDataLoader())
3835
return registry
3936
}
4037
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.examples.server.ktor.schema.dataloaders
18+
19+
import com.expediagroup.graphql.examples.server.ktor.schema.models.Book
20+
import com.expediagroup.graphql.server.execution.KotlinDataLoader
21+
import kotlinx.coroutines.runBlocking
22+
import org.dataloader.DataLoader
23+
import java.util.concurrent.CompletableFuture
24+
25+
val BookDataLoader = object : KotlinDataLoader<List<Long>, List<Book>> {
26+
override val dataLoaderName = "BATCH_BOOK_LOADER"
27+
override fun getDataLoader() = DataLoader<List<Long>, List<Book>> { ids ->
28+
CompletableFuture.supplyAsync {
29+
val allBooks = runBlocking { Book.search(ids.flatten()).toMutableList() }
30+
// produce lists of results from returned books
31+
ids.fold(mutableListOf()) { acc: MutableList<List<Book>>, idSet ->
32+
val matchingBooks = allBooks.filter { idSet.contains(it.id) }
33+
acc.add(matchingBooks)
34+
acc
35+
}
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.examples.server.ktor.schema.dataloaders
18+
19+
import com.expediagroup.graphql.examples.server.ktor.schema.models.Course
20+
import com.expediagroup.graphql.server.execution.KotlinDataLoader
21+
import kotlinx.coroutines.runBlocking
22+
import org.dataloader.DataLoader
23+
import java.util.concurrent.CompletableFuture
24+
25+
val CourseDataLoader = object : KotlinDataLoader<Long, Course?> {
26+
override val dataLoaderName = "COURSE_LOADER"
27+
override fun getDataLoader() = DataLoader<Long, Course?> { ids ->
28+
CompletableFuture.supplyAsync {
29+
runBlocking { Course.search(ids).toMutableList() }
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.examples.server.ktor.schema.dataloaders
18+
19+
import com.expediagroup.graphql.examples.server.ktor.schema.models.University
20+
import com.expediagroup.graphql.server.execution.KotlinDataLoader
21+
import kotlinx.coroutines.runBlocking
22+
import org.dataloader.DataLoader
23+
import java.util.concurrent.CompletableFuture
24+
25+
val UniversityDataLoader = object : KotlinDataLoader<Long, University?> {
26+
override val dataLoaderName = "UNIVERSITY_LOADER"
27+
override fun getDataLoader() = DataLoader<Long, University?> { ids ->
28+
CompletableFuture.supplyAsync {
29+
runBlocking { University.search(ids).toMutableList() }
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)