Hello, how do you handle N+1 problem? I know about...
# graphql-kotlin
n
Hello, how do you handle N+1 problem? I know about data loaders, but they have a significant problem - they can’t be chained https://github.com/graphql-java/graphql-java/issues/1078 For example, if I have some field, that relies on two data loaders, it may not work. @Dariusz Kuc I’ve heard you don’t use them, so how do you handle it? Can you provide some code? I think it might be a useful example
l
yeah i asked this question a while back: https://kotlinlang.slack.com/archives/CQLNT7B29/p1589478150040100 … i’m currently using dataloader in a limited fashion and would love to see some example code for caching/batching with coroutines
d
unfortunately don’t have a good answer to that
in general it resolves around properly structuring your code to avoid redundant calls
(was thinking about doing a post that goes into more detail about it)
for example lets assume you have simple schema like
Copy code
type Query {
  products: [Products]
}

type Product {
  description: ProductDescription
  price: ProductPrice
  reviews: [ProductReview]
}
so traditionally you would do something like
Copy code
fun products(): List<Product> {
   // fetch product ids
   // fetch description
   // fetch price
   // fetch reviews
   return listOf(products)
}
or something like that
you could destructure it so each
Product
would expose those fields as functions
Copy code
class Product {
  fun description(): ProductDescription { ... }
  fun price(): ProductPrice { ... }
  fun reviews(): List<ProductReview> { ... }
}
but then you end up -> 1 call to get product IDs and then each one of the products individual calls
so it becomes problematic if your underlying services don’t support batch apis
afaik data loader can be used to batch those calls but as you mentioned it might not work in all cases
there is also another case when multiple fields within given
Product
are calculated based on some common data -> if you expose those fields as functions (so they are only calculated when requested) how do you share the common data?
in this case the pattern we saw that was pretty useful was to use deferred variables to invoke common service, e.g.
Copy code
class Product {
  private val deferredServiceData = async { 
    slowCallGoesHere
  }

  fun description(): ProductDescription {
    val sharedData = deferredServiceData.await()
    // other logic
    ... 
  }

  fun price(): ProductPrice {
     val sharedData = deferredServiceData.await() // reuses same
     // price logic
     ...
  }
  
  ...
}
but yes this can all get somewhat complex pretty fast
we just started a discussion about how to simplify this (guess good timing?)
n
so it becomes problematic if your underlying services don’t support batch apis
but what if this underlying services support batching api? For example if price field requires a network call to ReviewService, and this service has a method like this:
Copy code
fun getReviews(productIds: List<Long>)
In this case, we still don’t have a solution to this problem, since that would be a request for every product?
In case of dataloader, it can track what ids were requested, and batch them together
then, we may need this reviews in some other query, that already have information about product, for example
Copy code
analyttics {
    reviews {
      star
    }
  }
d
well if underlying service does support batching you would do the first one
Copy code
fun products(): List<Product> {
   // fetch product ids
   // fetch description
   // fetch price
   // fetch reviews
   return listOf(products)
}
n
if we have a query like this
Copy code
analytics {
    reviews {
      star
    }
  }
  products {
    reviews {
      star
    }
  }
that will be handled too, and there will be only 1 network call
d
yes but again it depends how you structure your graph
in our use cases
analytics
and
products
would generally reside in separate microservices
n
Do you primarily use functionDataFetcher in your projects? If our
product
needs to access external api in order to fetch
reviews
, will be some
reviewsClient
injected right into it?
d
yes
n
maybe we need some recommended approach or generic example of it, when I first started using this library, I couldn’t figure it out, I think it’s uncommon approach, especially for someone who was using REST. We are used to consider such objects as some kind of DTO
👍 1
it depends how you structure your graph
So, if my graph may contain different objects, that use some shared source of data, is there any way to make less external calls? These objects could contain different arguments that were passed by our clients, or resolved during execution. I’m not sure if deffered variable can help here
d
In your
analytics
and
products
example I don’t think some common deferred would work as they dont share common parent
there is always an option to inspect the query using execution environment (from top field) and then manually decide what to call - but again it would be manual and also wouldn’t help with 2 top level queries