graphql / graphql-js

A reference implementation of GraphQL for JavaScript
http://graphql.org/graphql-js/
MIT License
20.04k stars 2.02k forks source link

[Feature requests]Allow resolve function returns a custom deferred object in order to do decent query optimization. #1416

Open JimChengLin opened 6 years ago

JimChengLin commented 6 years ago

I have read https://github.com/graphql/graphql-js/issues/26 , https://github.com/graphql/graphql-js/issues/700 and https://github.com/graphql/graphql-js/pull/304 . It seems like there is no well-agreed way to combine GraphQL and database. GraphQL.js loses a certain amount of chances to do query optimization. Am I right? I have a simple proposal. In the resolve function, we can return a DeferredObject instead of a normal JS object or Promise. Only in the very last moment, the deferred object queries the DB. Does this make sense?

The special DeferredObjects links with each other. So the minimal query fields needed can be detected. GraphQL.js may finally call await someDeferredObject.excute() multiple times. Since we have ES6 getter and setter. I think it may be quite seamless to current code base.

IvanGoncharov commented 6 years ago

@JimChengLin Problem is that graphql-js needs some runtime information in order to call correct resolvers. For example, if you use interfaces or unions than without knowing the actual type of every object you can't decide what resolvers you should call:

{
  hero {
    name
    ... on Human {
      homePlanet
    }
    ... on Droid {
      primaryFunction
    }
  }
}

Moreover, in the case of arrays, you need to know the exact size of array and type of every item.

Taking the above limitations into account I don't think we could achieve any significant speedup from using proxy objects.

JimChengLin commented 6 years ago

@IvanGoncharov The type can be known from some meta attributes. My proposal is simply "lazy-evaluation". When you really need it, you can still call await someDerferredObject.execute(). It wouldn't harm anything.

Example taking from https://github.com/iamshaunjp/graphql-playlist,

const BookType = new GraphQLObjectType({
    name: 'Book',
    fields: ( ) => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        genre: { type: GraphQLString },
        author: {
            type: AuthorType,
            resolve(parent, args){
                console.log(parent);
                return _.find(authors, { id: parent.authorId });
            }
        }
    })
});

In the resolve function of AuthorType, why do we need to know everything of the parent? Ideally, if parent is a deferred object, we can chain deferred operators.

Someone may call return parent.some_defferred_operators(xxx) that produces another deferred object. You follow with me? In the last stage, the author of the deferred object library like me has a full view of what data is needed, then we can have a single very efficient SQL query.

I don't expect the transformation would be really difficult, cuz query is generally Read-Only.