Проблема блокировки graphql- kotlin с помощью DataLoader & BatchLoader - PullRequest
1 голос
/ 15 марта 2020

Я использовал фреймворк "https://github.com/ExpediaGroup/graphql-kotlin", чтобы изучить программирование graphql для kotlin под springframework.

Я использовал DataLoader & BatchLoader для решения проблемы загрузки N + 1.

Когда область действия объектов DataLoader является одноэлементной, это работает, но это не моя цель, поскольку механизм временного кэширования не должен перекрывать разные запросы.

Затем я изменил область действия объектов DataLoader, чтобы В прототипе, по всей вероятности, запрос graphql может быть блокирован, и связанные объекты не будут загружены, клиент будет ждать ответа вечно.

В чем причина и как я могу ее решить?


Я сделал это так:

  1. Создайте простое приложение Springboot, добавьте Maven-зависимость graph- kotlin
<dependency>
   <groupId>com.expediagroup</groupId>
   <artifactId>graphql-kotlin-spring-server</artifactId>
   <version>2.0.0.RC3</version>
</dependency>
Создание двух классов моделей (Примечание. Их код будет изменен на последнем этапе)
data class Department(
    val id: Long,
    val name: String
)
data class Employee(
    val id: Long,
    val name: String,
    @GraphQLIgnore val departmentId: Long
)
Создание двух макетированных объектов репозитория
val DEPARTMENTS = listOf(
    Department(1L, "Develop"),
    Department(2L, "Test")
)
val EMPLOYEES = listOf(
    Employee(1L, "Jim", 1L),
    Employee(2L, "Kate", 1L),
    Employee(3L, "Tom", 2L),
    Employee(4L, "Mary", 2L)
)

@Repository
open class DepartmentRepository {

    companion object {
        private val LOGGER = LoggerFactory.getLogger(DepartmentRepository::class.java)
    }

    open fun findByName(namePattern: String?): List<Department> = //For root query
        namePattern
            ?.takeIf { it.isNotEmpty() }
            ?.let { pattern ->
                DEPARTMENTS.filter { it.name.contains(pattern) }
            }
            ?: DEPARTMENTS

    open fun findByIds(ids: Collection<Long>): List<Department> { // For assciation
        LOGGER.info("BatchLoad departments by ids: [${ids.joinToString(", ")}]")
        return DEPARTMENTS.filter { ids.contains(it.id) }
    }
}

@Repository
open class EmployeeRepository {

    companion object {
        private val LOGGER = LoggerFactory.getLogger(EmployeeRepository::class.java)
    }

    open fun findByName(namePattern: String?): List<Employee> = //For root query
        namePattern
            ?.takeIf { it.isNotEmpty() }
            ?.let { pattern ->
                EMPLOYEES.filter { it.name.contains(pattern) }
            }
            ?: EMPLOYEES

    open fun findByDepartmentIds(departmentIds: Collection<Long>): List<Employee> { // For association
        LOGGER.info("BatchLoad employees by departmentIds: [${departmentIds.joinToString(", ")}]")
        return EMPLOYEES.filter { departmentIds.contains(it.departmentId) }
    }
}
Создание объекта запроса graphql для экспорта root операций запроса
@Service
open class OrgService(
    private val departmentRepository: DepartmentRepository,
    private val employeeRepository: EmployeeRepository
) : Query {

    fun departments(namePattern: String?): List<Department> =
       departmentRepository.findByName(namePattern)

    fun employees(namePattern: String?): List<Employee> =
        employeeRepository.findByName(namePattern)
}
Создание абстрактного класса для загрузки многозначного связанного объекта
abstract class AbstractReferenceLoader<K, R: Any> (
    batchLoader: (Collection<K>) -> Collection<R>,
    keyGetter: (R) ->K,
    optionsInInitializer: (DataLoaderOptions.() -> Unit) ? = null
): DataLoader<K, R?>(
    { keys ->
        CompletableFuture.supplyAsync {
            batchLoader(keys)
                .associateBy(keyGetter)
                .let { map ->
                    keys.map { map[it] }
                }
        }
    },
    optionsInInitializer?.let {
        DataLoaderOptions().apply {
            this.it()
        }
    }
)
Создание абстрактного класса для загрузки связанной коллекции один-ко-многим
abstract class AbstractListLoader<K, E>(
    batchLoader: (Collection<K>) -> Collection<E>,
    keyGetter: (E) ->K,
    optionsInInitializer: (DataLoaderOptions.() -> Unit) ? = null
): DataLoader<K, List<E>>(
    { keys ->
        CompletableFuture.supplyAsync {
            batchLoader(keys)
                .groupBy(keyGetter)
                .let { map ->
                    keys.map { map[it] ?: emptyList() }
                }
        }
    },
    optionsInInitializer?.let {
        DataLoaderOptions().apply {
            this.it()
        }
    }
)
Создайте аннотацию, чтобы позволить bean-компонентам SpringLider Manager по объему прототипа
@Retention(RetentionPolicy.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Component
@Scope(
    ConfigurableBeanFactory.SCOPE_PROTOTYPE,
    proxyMode = ScopedProxyMode.NO
)
annotation class DataLoaderComponent
Создание bean-компонента loader для загрузки ссылки на родительский объект объекта Employee
@DataLoaderComponent
open class DepartmentLoader(
    private val departmentRepository: DepartmentRepository
): AbstractReferenceLoader<Long, Department>(
    { departmentRepository.findByIds(it) },
    { it.id },
    { setMaxBatchSize(256) }
)
Создание bean-компонента loader для загрузки коллекции дочерних объектов объекта Department
@DataLoaderComponent
open class EmployeeListByDepartmentIdLoader(
    private val employeeRepository: EmployeeRepository
): AbstractListLoader<Long, Employee>(
    { employeeRepository.findByDepartmentIds(it) },
    { it.departmentId },
    { setMaxBatchSize(16) }
)
Создайте конфигурацию GraphQL, чтобы «graphql- kotlin» знал все компоненты DataLoader
@Configuration
internal abstract class GraphQLConfig {

    @Bean
    open fun dataLoaderRegistryFactory(): DataLoaderRegistryFactory =
        object: DataLoaderRegistryFactory {
            override fun generate(): DataLoaderRegistry = dataLoaderRegistry()
        }

    @Bean
    @Scope(
        ConfigurableBeanFactory.SCOPE_PROTOTYPE,
        proxyMode = ScopedProxyMode.NO
    )
    protected open fun dataLoaderRegistry(
        loaders: List<DataLoader<*, *>>
    ): DataLoaderRegistry =
        DataLoaderRegistry().apply {
            loaders.forEach { loader ->
                register(
                    loader::class.qualifiedName,
                    loader
                )
            }
        }

    @Lookup
    protected abstract fun dataLoaderRegistry(): DataLoaderRegistry
}
Добавление методов буксировки в DataFetchingEnvironment для получения объектов DataLoader
inline fun <reified L: AbstractReferenceLoader<K, R>, K, R> DataFetchingEnvironment.getReferenceLoader(
): DataLoader<K, R?> =
    this.getDataLoader<K, R?>(L::class.qualifiedName)

inline fun <reified L: AbstractListLoader<K, E>, K, E> DataFetchingEnvironment.getListLoader(
): DataLoader<K, List<E>> =
    this.getDataLoader<K, List<E>>(L::class.qualifiedName)
Измените код отдела и сотрудника, разрешите им поддерживать ассоциацию
data class Department(
    val id: Long,
    val name: String
) {
    suspend fun employees(env: DataFetchingEnvironment): List<Employee> =
        env
            .getListLoader<EmployeeListByDepartmentIdLoader, Long, Employee>()
            .load(id)
            .await()
}

data class Employee(

    val id: Long,
    val name: String,

    @GraphQLIgnore val departmentId: Long
) {
    suspend fun department(env: DataFetchingEnvironment): Department? =
        env
            .getReferenceLoader<DepartmentLoader, Long, Department>()
            .load(departmentId)
            .await()
}

Build & Run

  1. Запустите приложения SpringBoot, откройте http://locathost: 8080 / детская площадка .
  2. Выполните запрос, результат может быть успешным или неудачным!
{
  employees {
    id
    name
    department {
      id
      name
      employees {
        id
        name
      }
    }
  }
}
В случае успеха клиент может получить ответ, а журнал сервера -
2020-03-15 22:47:26.366  INFO 35616 --- [onPool-worker-5] org.frchen.dal.DepartmentRepository      : BatchLoad departments by ids: [1, 2]
2020-03-15 22:47:26.367  INFO 35616 --- [onPool-worker-5] org.frchen.dal.EmployeeRepository        : BatchLoad employees by departmentIds: [1, 2]
Если это не удалось, клиент блокирует и ждет ответа навсегда, а журнал сервера -
2020-03-15 22:53:43.159  INFO 35616 --- [onPool-worker-6] org.frchen.dal.DepartmentRepository      : BatchLoad departments by ids: [1, 2]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...