Why DataFetchers that use Dataloaders for lateinit variables should have prototype beans and not singletons? #983
-
@Component("LastUpdatedDataFetcher")
@Scope(SCOPE_PROTOTYPE)
class LastUpdatedDataFetcher : DataFetcher<CompletableFuture<LastUpdated>>, BeanFactoryAware {
private lateinit var beanFactory: BeanFactory
override fun setBeanFactory(beanFactory: BeanFactory) {
this.beanFactory = beanFactory
}
override fun get(environment: DataFetchingEnvironment): CompletableFuture<LastUpdated> {
val companyId: UUID = environment.getSource<WithLastUpdatedDataloader>().companyId
return environment
.getDataLoader<UUID, LastUpdated>(LastUpdatedDataloader.NAME)
.load(companyId)
}
} Like, they're stateless anyway. What's the point?
data class DashboardReady(
@GraphQLIgnore
override var companyId: UUID,
override var status: ProcessingStatus = ProcessingStatus.READY
): Dashboard, WithLastUpdatedDataloader {
fun lastUpdated(environment: DataFetchingEnvironment): CompletableFuture<LastUpdated> {
return environment
.getDataLoader<UUID, LastUpdated>(LastUpdatedDataloader.NAME)
.load(companyId)
}
} I see only code reuse purpose with something very generic like DB Models, but then it requires you to introduce some marker interfaces as I have above
@Configuration
class DataLoaderConfiguration(private val lastUpdatedLoader: LastUpdatedDataloader) {
@Bean
fun dataLoaderRegistryFactory(): DataLoaderRegistryFactory {
return object : DataLoaderRegistryFactory {
override fun generate(): DataLoaderRegistry {
val registry = DataLoaderRegistry()
registry.register(LastUpdatedDataloader.NAME, lastUpdatedLoader.getLoader())
return registry
}
}
}
} Plus this dataloader component defenition @Component
class LastUpdatedDataloader(var lastUpdatedService: LastUpdatedService) {
companion object {
const val NAME: String = "lastUpdatedDataloader"
}
fun getLoader(): DataLoader<UUID, LastUpdated> {
return DataLoader.newMappedDataLoader { companyIds ->
CompletableFuture.supplyAsync {
companyIds.map { it to lastUpdatedService.getLastUpdatedByCompanyId(it) }.toMap()
}
}
}
} And in place of use of data loader data class DashboardReady(
@GraphQLIgnore
override var companyId: UUID,
override var status: ProcessingStatus = ProcessingStatus.READY
): Dashboard {
fun lastUpdated(environment: DataFetchingEnvironment): CompletableFuture<LastUpdated> {
return environment
.getDataLoader<UUID, LastUpdated>(LastUpdatedDataloader.NAME)
.load(companyId)
}
} Will be absolutely enough to get Dataloader working just fine? My main concern with While for some basic stuff like optimize n+1 for regular DB foreign key fetching of Models, this may work wonders (while still requires annoying interfaces to cast stuff), for one-off optimizations that just ensure sort of memoization, like if some service function called in this same request with same args and does some heavy computations, just resolve promise once and then return from the cache to all callers, I guess the approach in point 3 is the best. Sorry for a lot of text, just want to understand it better :) |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 4 replies
-
@RIP21 We don't have a great diagram but hopefully this can better explain what code creates the data fetchers. For properties, it is done here: And for functions it is done here: Everything else around the factories and providers is just to allow customization at various points and if you are using the spring-server, to do that with spring beans. As for BeanFactoryAware is used if you want |
Beta Was this translation helpful? Give feedback.
-
Great questions! As Shane mentioned, when building out GraphQL schema That being said, in general I think that is an overkill to create those custom data fetchers and I'd just retrieve the target data loader from the -- I think there was also some outstanding issue with mixing |
Beta Was this translation helpful? Give feedback.
Great questions!
As Shane mentioned, when building out GraphQL schema
graphql-kotlin
will automatically createFunctionDataFetcher
for functions/methods and default toPropertyDataFetcher
for getter fields. This mapping is stored ingraphql-java
CodeRegistry
that is then referenced when resolving fields. By marking a field aslateinit
it allows you to bypass the above default behavior and inject a custom data fetcher (i.e. yourLastUpdatedDataFetcher
) that abstracts away of how you resolve that field. If you create separate custom data fetcher (LastUpdatedDataFetcher
) it allows you to reuse it in multiple places. In the example above there is no point in making the custom data fetcher bea…