Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add example how to use data loaders with suspendable functions #1678

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 93 additions & 4 deletions website/docs/server/data-loader/data-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,97 @@ class User(val id: ID) {
}
```

:::info
Given that `graphql-java` relies on `CompletableFuture`s for scheduling and asynchronous execution of `DataLoader` calls,
currently we don't provide any native support for `DataLoader` pattern using coroutines. Instead, return
## DataLoaders and Coroutines

`graphql-java` relies on `CompletableFuture` for scheduling and execute asynchronous field resolvers (aka data fetchers),
`graphql-java` deliberately aims for near zero dependencies which means no Kotlin / WebFlux / RxJava.

Similar to `asynchronous` field resolvers (aka data fetcher), `DataLoader` pattern implementation in `graphql-java` works with `CompletableFuture`,
and because of the listed `graphql-java` constrains we don't provide any native support for `DataLoader` pattern using suspendable functions. Instead, return
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved
the `CompletableFuture` directly from your `DataLoader`s. See issue [#986](https://github.com/ExpediaGroup/graphql-kotlin/issues/986).
:::

### Example

Consider the following query:

```graphql
fragment UserFragment on User {
id
name
}
query GetUsersFriends {
user_1: user(id: 1) {
...UserFragment
}
user_2: user(id: 2) {
...UserFragment
}
}
```

And the corresponding code that will autogenerate schema:

```kotlin
class MyQuery(
private val userService: UserService
) : Query {
suspend fun getUser(id: Int): User = userService.getUser(id)
}

class UserService {
suspend fun getUser(id: Int): User = // async logic to get user
suspend fun getUsers(ids: List<Int>): List<User> = // async logic to get users
}
```

When the execution of the query completes we will have a total of 2 requests to the `UserService`, this is where the usage of
`DataLoader` pattern can help to make the execution of query more performant by making only 1 request.
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved

Lets create the `UserDataLoader`:

```kotlin
class UserDataLoader : KotlinDataLoader<ID, User> {
override val dataLoaderName = "UserDataLoader"
override fun getDataLoader() =
DataLoaderFactory.newDataLoader<Int, User> { ids, batchLoaderEnvironment ->
val coroutineScope =
batchLoaderEnvironment.getGraphQLContext()?.get<CoroutineScope>()
?: CoroutineScope(EmptyCoroutineContext)

coroutineScope.future {
userService.getUsers(ids)
}
}
}
```

There are some things going on here:

1. We define the `UserDataLoader` with name "UserDataLoader"
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved
2. The `getLoader()` method returns a `DataLoader<Int, User>`, which `BatchLoader` function should return a `List<User>`
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved
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`, which is provided in the `DataFetchingEnvironment` and accessible with the [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.
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved
4. After retrieving the `CoroutineScope` from the `batchLoaderEnvironment` we will be able to execute the `userService.getUsers(ids)` suspendable function.
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)

Finally, the only thing that we need to change is the `user` field resolver, to not be suspendable and
just return the `CompletableFuture<User>` that the `DataLoader`, make sure to pass the `dataFetchingEnvironment` as `keyContext` which is the second argument of `DataLoader.load`
samuelAndalon marked this conversation as resolved.
Show resolved Hide resolved

```kotlin
class MyQuery(
private val userService: UserService
) : Query {
suspend fun getUser(id: Int, dataFetchingEnvironment: DataFetchingEnvironment): User =
dataFetchingEnvironment
.getDataLoader<Int, Mission>("UserDataLoader")
.load(id, dataFetchingEnvironment)
}

class UserService {
suspend fun getUser(id: Int): User {
// logic to get user
}
suspend fun getUsers(ids: List<Int>): List<User> {
// logic to get users, this method is called from the DataLoader
}
}
```