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
Show file tree
Hide file tree
Changes from all commits
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
103 changes: 98 additions & 5 deletions website/docs/server/data-loader/data-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,101 @@ 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
the `CompletableFuture` directly from your `DataLoader`s. See issue [#986](https://github.com/ExpediaGroup/graphql-kotlin/issues/986).
:::
## DataLoaders and Coroutines

`graphql-java` relies on `CompletableFuture`s for scheduling and asynchronously executing GraphQL operations.
While we can provide native support for coroutines for data fetchers (aka field resolvers) because they are resolved
independently, we cannot easily provide native support for the `DataLoader` pattern as it relies
on `CompletableFuture` state machine internals and we cannot update it to use coroutines without fully rewriting
GraphQL Java execution engine.

If you would like to use `DataLoader` pattern in your project, you have to update your data fetchers (aka field resolvers) to return
`CompletableFuture` from the invoked `DataLoader`.

### 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 we execute the above query, we will end up calling `UserService#getUser` twice which will result in two independent
downstream service/database calls. This problem is called N+1 problem. By using `DataLoader` pattern,
we can solve this problem and only make a single downstream request/query.

Lets create the `UserDataLoader`:

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

coroutineScope.future { // 5
userService.getUsers(ids)
}
}
}

```

There are some things going on here:

1. We define the `UserDataLoader` with name "UserDataLoader".
2. The `KotlinDataLoader#getDataLoader()` method returns a `DataLoader<Int, User>`, which `BatchLoader` function should return a `List<User>`.
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.
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, we need to update `user` field resolver, to return the `CompletableFuture<User>` from the invoked `DataLoader`.
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.

```kotlin
class MyQuery(
private val userService: UserService
) : Query {
fun getUser(id: Int, dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<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
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,101 @@ 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
the `CompletableFuture` directly from your `DataLoader`s. See issue [#986](https://github.com/ExpediaGroup/graphql-kotlin/issues/986).
:::
## DataLoaders and Coroutines

`graphql-java` relies on `CompletableFuture`s for scheduling and asynchronously executing GraphQL operations.
While we can provide native support for coroutines for data fetchers (aka field resolvers) because they are resolved
independently, we cannot easily provide native support for the `DataLoader` pattern as it relies
on `CompletableFuture` state machine internals and we cannot update it to use coroutines without fully rewriting
GraphQL Java execution engine.

If you would like to use `DataLoader` pattern in your project, you have to update your data fetchers (aka field resolvers) to return
`CompletableFuture` from the invoked `DataLoader`.

### 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 we execute the above query, we will end up calling `UserService#getUser` twice which will result in two independent
downstream service/database calls. This problem is called N+1 problem. By using `DataLoader` pattern,
we can solve this problem and only make a single downstream request/query.

Lets create the `UserDataLoader`:

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

coroutineScope.future { // 5
userService.getUsers(ids)
}
}
}

```

There are some things going on here:

1. We define the `UserDataLoader` with name "UserDataLoader".
2. The `KotlinDataLoader#getDataLoader()` method returns a `DataLoader<Int, User>`, which `BatchLoader` function should return a `List<User>`.
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.
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, we need to update `user` field resolver, to return the `CompletableFuture<User>` from the invoked `DataLoader`.
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.

```kotlin
class MyQuery(
private val userService: UserService
) : Query {
fun getUser(id: Int, dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<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
}
}
```