morpheus-graphql
morpheus-graphql copied to clipboard
Batching of resolvers / data fetching
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 sorry did not got. can you give me an example?
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?
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
.
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.
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.
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
.
@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.
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:
-
placesVisited
is resolved lazily (places are fetched only when requested) - No N + 1 selects
I accidentaly closed this issue. Re-opening.
placesVisited
is resolved lazily (places are fetched only when requested)- 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.
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
.
@dandoh, actally i am planning to add new feature getContext which will you give access to current internal state of resolving. see, #372
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 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"
...
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.
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
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 great idea :) i would love to read it
@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
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.
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...
sorry. but union types in selection does not work that way.
but we can define virtual type`SelectionNode' that can support it. another idea is.
getChildren :: node -> [(Maybe ConditionTypeName, node)]
or just
getChildren :: node -> Either [(ConditionTypeName, [node])] [node]
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
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.
another solution can be:
getChildren :: Maybe TypeName -> node -> [node]
where Nothing means give me selection without any condition
@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 you should show me what you mean. please give a concrete example like @smatting
@smatting @dandoh @AnthonySuper @Herlevsen i think i will provide batching api similar to https://github.com/graphql/dataloader