Skip to content

Commit a866996

Browse files
chore: add example how to use data loaders with suspendable functions (#1678)
* feat: add example data loaders with suspendable functions * chore: address comments * chore: address comments * chore: update 6.x.x docs as well * chore: address comments
1 parent 10e9d84 commit a866996

File tree

2 files changed

+196
-10
lines changed

2 files changed

+196
-10
lines changed

website/docs/server/data-loader/data-loader.md

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,101 @@ class User(val id: ID) {
9797
}
9898
```
9999

100-
:::info
101-
Given that `graphql-java` relies on `CompletableFuture`s for scheduling and asynchronous execution of `DataLoader` calls,
102-
currently we don't provide any native support for `DataLoader` pattern using coroutines. Instead, return
103-
the `CompletableFuture` directly from your `DataLoader`s. See issue [#986](https://github.com/ExpediaGroup/graphql-kotlin/issues/986).
104-
:::
100+
## DataLoaders and Coroutines
101+
102+
`graphql-java` relies on `CompletableFuture`s for scheduling and asynchronously executing GraphQL operations.
103+
While we can provide native support for coroutines for data fetchers (aka field resolvers) because they are resolved
104+
independently, we cannot easily provide native support for the `DataLoader` pattern as it relies
105+
on `CompletableFuture` state machine internals and we cannot update it to use coroutines without fully rewriting
106+
GraphQL Java execution engine.
107+
108+
If you would like to use `DataLoader` pattern in your project, you have to update your data fetchers (aka field resolvers) to return
109+
`CompletableFuture` from the invoked `DataLoader`.
110+
111+
### Example
112+
113+
Consider the following query:
114+
115+
```graphql
116+
fragment UserFragment on User {
117+
id
118+
name
119+
}
120+
query GetUsersFriends {
121+
user_1: user(id: 1) {
122+
...UserFragment
123+
}
124+
user_2: user(id: 2) {
125+
...UserFragment
126+
}
127+
}
128+
```
129+
130+
And the corresponding code that will autogenerate schema:
131+
132+
```kotlin
133+
class MyQuery(
134+
private val userService: UserService
135+
) : Query {
136+
suspend fun getUser(id: Int): User = userService.getUser(id)
137+
}
138+
139+
class UserService {
140+
suspend fun getUser(id: Int): User = // async logic to get user
141+
suspend fun getUsers(ids: List<Int>): List<User> = // async logic to get users
142+
}
143+
```
144+
145+
When we execute the above query, we will end up calling `UserService#getUser` twice which will result in two independent
146+
downstream service/database calls. This problem is called N+1 problem. By using `DataLoader` pattern,
147+
we can solve this problem and only make a single downstream request/query.
148+
149+
Lets create the `UserDataLoader`:
150+
151+
```kotlin
152+
class UserDataLoader : KotlinDataLoader<ID, User> {
153+
override val dataLoaderName = "UserDataLoader" // 1
154+
override fun getDataLoader() = // 2
155+
DataLoaderFactory.newDataLoader<Int, User> { ids, batchLoaderEnvironment ->
156+
val coroutineScope = // 3
157+
batchLoaderEnvironment.getGraphQLContext()?.get<CoroutineScope>()
158+
?: CoroutineScope(EmptyCoroutineContext) // 4
159+
160+
coroutineScope.future { // 5
161+
userService.getUsers(ids)
162+
}
163+
}
164+
}
165+
166+
```
167+
168+
There are some things going on here:
169+
170+
1. We define the `UserDataLoader` with name "UserDataLoader".
171+
2. The `KotlinDataLoader#getDataLoader()` method returns a `DataLoader<Int, User>`, which `BatchLoader` function should return a `List<User>`.
172+
3. Given that we **don't want** to change our `UserService` async model that is using coroutines, we need a `CoroutineScope`, [which is conveniently available](../../schema-generator/execution/async-models/#coroutines) in the `GraphQLContext` and accessible through [`DataFetchingEnvironment#getGraphQLContext()`](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/extensions/BatchLoaderEnvironmentExtensions.kt#L43) extension function.
173+
4. After retrieving the `CoroutineScope` from the `batchLoaderEnvironment` we will be able to execute the `userService.getUsers(ids)` suspendable function.
174+
5. We interoperate the suspendable function result to a `CompletableFuture` using [coroutineScope.future](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/future.html).
175+
176+
Finally, we need to update `user` field resolver, to return the `CompletableFuture<User>` from the invoked `DataLoader`.
177+
Make sure to update method signature to also accept the `dataFetchingEnvironment` as you need to pass it to `DataLoader#load` method to be able to execute the request in appropriate coroutine scope.
178+
179+
```kotlin
180+
class MyQuery(
181+
private val userService: UserService
182+
) : Query {
183+
fun getUser(id: Int, dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<User> =
184+
dataFetchingEnvironment
185+
.getDataLoader<Int, Mission>("UserDataLoader")
186+
.load(id, dataFetchingEnvironment)
187+
}
188+
189+
class UserService {
190+
suspend fun getUser(id: Int): User {
191+
// logic to get user
192+
}
193+
suspend fun getUsers(ids: List<Int>): List<User> {
194+
// logic to get users, this method is called from the DataLoader
195+
}
196+
}
197+
```

website/versioned_docs/version-6.x.x/server/data-loader/data-loader.md

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,101 @@ class User(val id: ID) {
9797
}
9898
```
9999

100-
:::info
101-
Given that `graphql-java` relies on `CompletableFuture`s for scheduling and asynchronous execution of `DataLoader` calls,
102-
currently we don't provide any native support for `DataLoader` pattern using coroutines. Instead, return
103-
the `CompletableFuture` directly from your `DataLoader`s. See issue [#986](https://github.com/ExpediaGroup/graphql-kotlin/issues/986).
104-
:::
100+
## DataLoaders and Coroutines
101+
102+
`graphql-java` relies on `CompletableFuture`s for scheduling and asynchronously executing GraphQL operations.
103+
While we can provide native support for coroutines for data fetchers (aka field resolvers) because they are resolved
104+
independently, we cannot easily provide native support for the `DataLoader` pattern as it relies
105+
on `CompletableFuture` state machine internals and we cannot update it to use coroutines without fully rewriting
106+
GraphQL Java execution engine.
107+
108+
If you would like to use `DataLoader` pattern in your project, you have to update your data fetchers (aka field resolvers) to return
109+
`CompletableFuture` from the invoked `DataLoader`.
110+
111+
### Example
112+
113+
Consider the following query:
114+
115+
```graphql
116+
fragment UserFragment on User {
117+
id
118+
name
119+
}
120+
query GetUsersFriends {
121+
user_1: user(id: 1) {
122+
...UserFragment
123+
}
124+
user_2: user(id: 2) {
125+
...UserFragment
126+
}
127+
}
128+
```
129+
130+
And the corresponding code that will autogenerate schema:
131+
132+
```kotlin
133+
class MyQuery(
134+
private val userService: UserService
135+
) : Query {
136+
suspend fun getUser(id: Int): User = userService.getUser(id)
137+
}
138+
139+
class UserService {
140+
suspend fun getUser(id: Int): User = // async logic to get user
141+
suspend fun getUsers(ids: List<Int>): List<User> = // async logic to get users
142+
}
143+
```
144+
145+
When we execute the above query, we will end up calling `UserService#getUser` twice which will result in two independent
146+
downstream service/database calls. This problem is called N+1 problem. By using `DataLoader` pattern,
147+
we can solve this problem and only make a single downstream request/query.
148+
149+
Lets create the `UserDataLoader`:
150+
151+
```kotlin
152+
class UserDataLoader : KotlinDataLoader<ID, User> {
153+
override val dataLoaderName = "UserDataLoader" // 1
154+
override fun getDataLoader() = // 2
155+
DataLoaderFactory.newDataLoader<Int, User> { ids, batchLoaderEnvironment ->
156+
val coroutineScope = // 3
157+
batchLoaderEnvironment.getGraphQLContext()?.get<CoroutineScope>()
158+
?: CoroutineScope(EmptyCoroutineContext) // 4
159+
160+
coroutineScope.future { // 5
161+
userService.getUsers(ids)
162+
}
163+
}
164+
}
165+
166+
```
167+
168+
There are some things going on here:
169+
170+
1. We define the `UserDataLoader` with name "UserDataLoader".
171+
2. The `KotlinDataLoader#getDataLoader()` method returns a `DataLoader<Int, User>`, which `BatchLoader` function should return a `List<User>`.
172+
3. Given that we **don't want** to change our `UserService` async model that is using coroutines, we need a `CoroutineScope`, [which is conveniently available](../../schema-generator/execution/async-models/#coroutines) in the `GraphQLContext` and accessible through [`DataFetchingEnvironment#getGraphQLContext()`](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/extensions/BatchLoaderEnvironmentExtensions.kt#L43) extension function.
173+
4. After retrieving the `CoroutineScope` from the `batchLoaderEnvironment` we will be able to execute the `userService.getUsers(ids)` suspendable function.
174+
5. We interoperate the suspendable function result to a `CompletableFuture` using [coroutineScope.future](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/future.html).
175+
176+
Finally, we need to update `user` field resolver, to return the `CompletableFuture<User>` from the invoked `DataLoader`.
177+
Make sure to update method signature to also accept the `dataFetchingEnvironment` as you need to pass it to `DataLoader#load` method to be able to execute the request in appropriate coroutine scope.
178+
179+
```kotlin
180+
class MyQuery(
181+
private val userService: UserService
182+
) : Query {
183+
fun getUser(id: Int, dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<User> =
184+
dataFetchingEnvironment
185+
.getDataLoader<Int, Mission>("UserDataLoader")
186+
.load(id, dataFetchingEnvironment)
187+
}
188+
189+
class UserService {
190+
suspend fun getUser(id: Int): User {
191+
// logic to get user
192+
}
193+
suspend fun getUsers(ids: List<Int>): List<User> {
194+
// logic to get users, this method is called from the DataLoader
195+
}
196+
}
197+
```

0 commit comments

Comments
 (0)