Я использовал фреймворк "https://github.com/ExpediaGroup/graphql-kotlin", чтобы изучить программирование graphql для kotlin под springframework.
Я использовал DataLoader & BatchLoader для решения проблемы загрузки N + 1.
Когда область действия объектов DataLoader является одноэлементной, это работает, но это не моя цель, поскольку механизм временного кэширования не должен перекрывать разные запросы.
Затем я изменил область действия объектов DataLoader, чтобы В прототипе, по всей вероятности, запрос graphql может быть блокирован, и связанные объекты не будут загружены, клиент будет ждать ответа вечно.
В чем причина и как я могу ее решить?
Я сделал это так:
- Создайте простое приложение 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
- Запустите приложения SpringBoot, откройте http://locathost: 8080 / детская площадка .
- Выполните запрос, результат может быть успешным или неудачным!
{
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]