morpheus-graphql icon indicating copy to clipboard operation
morpheus-graphql copied to clipboard

Batching of resolvers / data fetching

Open smatting opened this issue 5 years ago • 35 comments

type Deity {
  name: String!,
  children: [Deity]
}

type Query {
  deities(q: String) : [Deity]
}

Is it possible to implement resolvers that fetch all children for all the deities in one batch? Let's assume that you can resolve the list of all deities in one call, too.

Currently I only know how to write a lazy resolver for children which would be called for every Deity found in the resolver of deities(q: String).

Could it by done by customizing the Monad m maybe?

smatting avatar Dec 29 '19 11:12 smatting

@smatting sorry did not got. can you give me an example?

nalchevanidze avatar Dec 29 '19 11:12 nalchevanidze

type Query {
  deities(q: String) : [Deity]
}
data Deity = Deity
  { name :: Text   
  , children   :: [Deity]
  } deriving (Generic,GQLType)

importGQLDocumentWithNamespace "schema.gql"

rootResolver :: GQLRootResolver IO () Query Undefined Undefined
rootResolver =
  GQLRootResolver
    {
      queryResolver = Query {queryDeities},
      mutationResolver = Undefined,
      subscriptionResolver = Undefined
    }
  where
    queryDeity QueryDeityArgs {queryDeityArgsName} = pure [ Deity {name ="", children=[...Deity...] },....]

do you mean this?

nalchevanidze avatar Dec 29 '19 11:12 nalchevanidze

type Place {
   name: String!
}

type Deity {
  name: String!,
  placesVisited: [Place]
}

type Query {
  deities(q: String) : [Deity]
}

Let's assume that I have to fetch the placesVisited from a separate database B and the resolver for deities(q: String) fetches 1000 deities from database A with one query. Then to resolve placesVisited I don't want to make 1000 queries to database B.

smatting avatar Dec 29 '19 11:12 smatting

data DBDeity = DBDeity { 
    dbID :: ID,
    dbName :: Name
}

dbDeities :: IO [DBDeity]
dbDeities = ....

dbPlacesByVisitors :: [ID] -> IO (Map ID Place)
dbPlacesByVisitors ids = ....

buildDeity ::   Map ID Place -> DBDeity -> Deity
buildDeity pls DBDeity{ id , dbName }  =  Diety {   
      places = maybe [] <$> lookup id pls,
      name = dbName 
}

deitiesResolver :: IORes e [Deity]
deitiesResolver = lift $ do
    dts <- dbDeities
    pls <- dbPlacesByVisitors (map dbID dts)
    pure $ map (buildDeity pls) dts

one way is that i think.

nalchevanidze avatar Dec 29 '19 12:12 nalchevanidze

but then you loose laziness. you ask places at db even if query does not needs it. may there is an automated way how they may batched, but i don't know yet.

nalchevanidze avatar Dec 29 '19 12:12 nalchevanidze

Could it by done by customizing the Monad m maybe?

monad that bundles all your db requests in one request could be a solution. i am not quit familiar with Haxl but may it could help.

one more thing what i'm thinking is this #258 proposal of @theobat, he is writing library that generates sql queries from schema + request.

nalchevanidze avatar Dec 29 '19 14:12 nalchevanidze

@smatting I believe we have complete control over this. We can make a Deity resolver:

deityResolver :: (DeityData, Maybe [PlaceData]) -> ResolverM Deity
deityResolver (deity, maybePlaces) = 
    Deity {..., placeVisited = placeVisitedResolver, ...}
  where 
    placeVisitedResolver = do 
        places <- case maybePlaces of 
            Just ps -> ps
            _ -> getDBDeityPlaces deity 
        mapM placeResolver places    

Then we know we can prefetch places for deities wherever we see fits:

getDBDeitiesWithPlaces :: IO [(DeityData, PlaceData)]
getDBDeitiesWithPlaces = ...

deitiesResolver :: ResolverM [Deity]
deitiesResolver = do 
    deitiesWithPlaces <- fmap (mapSnd Just) getDBDeitiesWithPlaces
    mapM deityResolver deitiesWithPlaces

No more N + 1 query.

In terms of getting additional query information (such as visitedPlaces above), https://github.com/tomjaguarpaw/haskell-opaley is a good choice.

dandoh avatar Jan 03 '20 04:01 dandoh

Thanks @dandoh! That's a cool trick to add optional prefetching to deityResolver, but deitiesWithPlaces is still loaded in every request, right? Your example solves the N+1 selects problem similar to @nalchevanidze 's example, but also sacrifices laziness of placesVisited (it's fetched even if not requested). Would be cool to have both:

  1. placesVisited is resolved lazily (places are fetched only when requested)
  2. No N + 1 selects

smatting avatar Jan 03 '20 11:01 smatting

I accidentaly closed this issue. Re-opening.

smatting avatar Jan 03 '20 11:01 smatting

  1. placesVisited is resolved lazily (places are fetched only when requested)
  2. No N + 1 selects

That would be really cool. @nalchevanidze What is the state of #258 ?

Is something like this work?

class InterpreterAST k m e where 
  interpreterAST :: Monad m => (RootResCon m e query mut sub) => 
    GQLRootResolver (ReaderT AST m) e query mut sub -> k

Then with access to query/mutation AST we can decide to prefetch or not.

dandoh avatar Jan 04 '20 02:01 dandoh

That would be really cool. @nalchevanidze What is the state of #258 ?

@dandoh, actally i am planning to add new feature getContext which will you give access to current internal state of resolving.

deitiesResolver :: IORes e [Deity]
deitiesResolver = do
    Context { selection } <- getContext
    dts <- lift dbDeities
    pls <- lift dbPlacesByVisitors selection (map dbID dts)
    pure $ map (buildDeity pls) dts

disadvantage is that resolver will depend on internal AST.

nalchevanidze avatar Jan 04 '20 12:01 nalchevanidze

@dandoh, actally i am planning to add new feature getContext which will you give access to current internal state of resolving. see, #372

nalchevanidze avatar Jan 06 '20 22:01 nalchevanidze

Currently we can sort of do preloading via the internal context. However, as you said, the internal context should be considered "unsafe" as it exposes internal details. Are there any plans for a more public version of this API? The basic case would be to have some sort of recursive query type that told you the fields requested and the arguments passed, so you could do preloading. Unfortunately I am having a lot of trouble coming up with a typesafe API to do so, but in principle one should be possible, I think?

AnthonySuper avatar Feb 09 '20 22:02 AnthonySuper

@AnthonySuper what if we define function that can search in sub selections.

path :: Text -> Resolver o m Bool
-- so you can ask
somRes = do
    (shouldPrefetch :: Bool) <- path "field1.field2"
    ...

nalchevanidze avatar Feb 10 '20 10:02 nalchevanidze

That would be a great start! Eventually I'd love to get something where we can get arguments to prefetch too, but messing around with that on my end has given me a nontrivial amount of trouble in figuring out how the types would work.

AnthonySuper avatar Feb 10 '20 14:02 AnthonySuper

The way some other graphql libraries solves batching, is by using the concept of a dataloader. The dataloader can batch requests to the same resources, and also handles caching to avoid unnecessary lookups. Facebooks implementation, for javascript/node is located here https://github.com/graphql/dataloader. They actually mention Haxl, and it seems to be the exact thing it was created for

Herlevsen avatar Feb 18 '20 10:02 Herlevsen

Haxl definitely seems to be a good option. I'm currently toying around with a simpler version that uses laziness (and unsafeInterleaveIO) to rewrite the actions in the graph, which seems to work out okay—I'll probably write a blog post about it if it might help out other people.

AnthonySuper avatar Feb 18 '20 17:02 AnthonySuper

@AnthonySuper great idea :) i would love to read it

nalchevanidze avatar Feb 18 '20 19:02 nalchevanidze

@AnthonySuper what if we define function that can search in sub selections.

path :: Text -> Resolver o m Bool
-- so you can ask
somRes = do
    (shouldPrefetch :: Bool) <- path "field1.field2"
    ...

Regarding this, I believe we should simply implement a typeclass instance of a Tree for the SelectionSet type (such as this one for example), this would likely simplify a few internal operations (print the AST back to a doc etc) and give simple (read canonical) utility functions to manipulate the query's AST while retaining flexibility for the concrete (internal) representation. I can probably try a PR for this.

Also it enables not exporting the concrete Rep while giving freedom for the end-user by exposing the Tree typeclass and its operations (which was your concern I believe @nalchevanidze)

theobat avatar May 10 '20 10:05 theobat

@theobat what incase of UnionSelection?

https://github.com/morpheusgraphql/morpheus-graphql/blob/68ac1a95d1238db5c23cd18f6ec6b70567c11ff1/morpheus-graphql-core/src/Data/Morpheus/Types/SelectionTree.hs#L35-L37

we may should add:

  • isUnion :: node -> Bool
  • getUnionChildren :: TypeName -> node -> [node]

ps. i think we should rename getChildrenList -> getChildren. is kind a plural anyway.

nalchevanidze avatar May 12 '20 13:05 nalchevanidze

Yep I'll change the name. As far as the union is concerned, is it actually separated in the selection set ? or mixed ? I think it'd be simpler to just give a getTypeName :: node -> TypeName kind of operation though...

theobat avatar May 13 '20 15:05 theobat

sorry. but union types in selection does not work that way.

nalchevanidze avatar May 13 '20 15:05 nalchevanidze

but we can define virtual type`SelectionNode' that can support it. another idea is.

getChildren ::  node -> [(Maybe ConditionTypeName, node)]

nalchevanidze avatar May 13 '20 15:05 nalchevanidze

or just

getChildren ::  node -> Either [(ConditionTypeName, [node])] [node]

nalchevanidze avatar May 13 '20 15:05 nalchevanidze

Sorry, I made a typo I meant getTypeName :: node -> Maybe TypeName. If the use case described is this:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

Then it should work, and I find it "more general" in the sense that union type or not, every graphql node has a type it inhabits

Note: but maybe the name is not precise enough then, it should probably be called getParentTypeName or something like that

theobat avatar May 13 '20 15:05 theobat

i meant union types in Morpheus GraphQL internaly union selection is not represented that way.

we you should wrap it as.

newtype SelectionNode = SelectionNode {  typeName:: TypeName, selection :: SelectionNode  }

and you need additionally the schema to get selection Type.

nalchevanidze avatar May 13 '20 15:05 nalchevanidze

another solution can be:

getChildren ::  Maybe TypeName ->  node -> [node]

where Nothing means give me selection without any condition

nalchevanidze avatar May 13 '20 15:05 nalchevanidze

@nalchevanidze @theobat And discussed with @nalchevanidze,

How hard is it to make another intepretation: resolver by field instead of by type as that of mu-haskell, on top of what we had?

User {
  name: String
  dog: Dog
}

Dog {
  name: string
}

From this we can derive the Selection type family:

data Selection String = Bool -- For any scalar
data Selection User = { name : Selection String, dog : Selection Dog }
data Selection Dog = { name : Selection String }

Then each resolver will have the coressponding Selection as an argument. https://github.com/higherkindness/mu-haskell/issues/190

Edit: Union types should receive product of selection types as arguments I think?

dandoh avatar May 13 '20 17:05 dandoh

@dandoh you should show me what you mean. please give a concrete example like @smatting

nalchevanidze avatar May 21 '20 21:05 nalchevanidze

@smatting @dandoh @AnthonySuper @Herlevsen i think i will provide batching api similar to https://github.com/graphql/dataloader

nalchevanidze avatar Aug 15 '20 13:08 nalchevanidze