35

I am learning GraphQL so I built a little project. Let's say I have 2 models, User and Comment.

const Comment = Model.define('Comment', {
  content: {
    type: DataType.TEXT,
    allowNull: false,
    validate: {
      notEmpty: true,
    },
  },
});

const User = Model.define('User', {
  name: {
    type: DataType.STRING,
    allowNull: false,
    validate: {
      notEmpty: true,
    },
  },
  phone: DataType.STRING,
  picture: DataType.STRING,
});

The relations are one-to-many, where a user can have many comments.
I have built the schema like this:

const UserType = new GraphQLObjectType({
  name: 'User',
  fields: () => ({
    id: {
      type: GraphQLString
    },
    name: {
      type: GraphQLString
    },
    phone: {
      type: GraphQLString
    },
    comments: {
      type: new GraphQLList(CommentType),
      resolve: user => user.getComments()
    }
  })
});

And the query:

const user = {
  type: UserType,
  args: {
    id: {
      type: new GraphQLNonNull(GraphQLString)
    }
  },
  resolve(_, {id}) => User.findById(id)
};

Executing the query for a user and his comments is done with 1 request, like so:

{
  User(id:"1"){
    Comments{
      content
    }
  }
}

As I understand, the client will get the results using 1 query, this is the benefit using GraphQL. But the server will execute 2 queries, one for the user and another one for his comments.

My question is, what are the best practices for building the GraphQL schema and types and combining join between tables, so that the server could also execute the query with 1 request?

Community
  • 1
  • 1
itaied
  • 6,827
  • 13
  • 51
  • 86

2 Answers2

16

The concept you are refering to is called batching. There are several libraries out there that offer this. For example:

  • Dataloader: generic utility maintained by Facebook that provides "a consistent API over various backends and reduce requests to those backends via batching and caching"

  • join-monster: "A GraphQL-to-SQL query execution layer for batch data fetching."

marktani
  • 7,578
  • 6
  • 37
  • 60
  • If you prefer video, here is a talk about join monster: https://www.youtube.com/watch?v=Y7AdMIuXOgs :) – marktani Dec 23 '16 at 11:33
  • Thank you for your post. Which one would you choose? On one hand I have facebook and 1800 stars library that batch some requests, but on the other hand I have a library that batch everything to 1 request. – itaied Jan 26 '17 at 19:13
  • We made good experiences with Dataloader at Graphcool, back when we were running a node GraphQL backend with graphql-js. Can't share any experience on join-monster. I think they slightly differ in features and usage so you would need to test it out I guess :) – marktani Jan 26 '17 at 19:41
  • Batching for me is something different! Batching means to take an array of IDs and (like in a 1 + N scenario) and get them all with one query. What join monster and [Photonjs](https://github.com/prisma/photonjs) do is something even better though, they leverage SQL joins to fulfil the GQL query with just a single SQL Query. – vanthome Nov 19 '19 at 16:42
6

To anyone using .NET and the GraphQL for .NET package, I have made an extension method that converts the GraphQL Query into Entity Framework Includes.

public static class ResolveFieldContextExtensions
{
    public static string GetIncludeString(this ResolveFieldContext<object> source)
    {
        return string.Join(',', GetIncludePaths(source.FieldAst));
    }

    private static IEnumerable<Field> GetChildren(IHaveSelectionSet root)
    {
        return root.SelectionSet.Selections.Cast<Field>()
                                           .Where(x => x.SelectionSet.Selections.Any());
    }

    private static IEnumerable<string> GetIncludePaths(IHaveSelectionSet root)
    {
        var q = new Queue<Tuple<string, Field>>();
        foreach (var child in GetChildren(root))
            q.Enqueue(new Tuple<string, Field>(child.Name.ToPascalCase(), child));

        while (q.Any())
        {
            var node = q.Dequeue();
            var children = GetChildren(node.Item2).ToList();
            if (children.Any())
            {
                foreach (var child in children)
                    q.Enqueue(new Tuple<string, Field>
                                  (node.Item1 + "." + child.Name.ToPascalCase(), child));

            }
            else
            {
                yield return node.Item1;
            }
        }}}

Lets say we have the following query:

query {
  getHistory {
    id
    product {
      id
      category {
        id
        subCategory {
          id
        }
        subAnything {
          id
        }
      }
    }
  }
}

We can create a variable in "resolve" method of the field:

var include = context.GetIncludeString();

which generates the following string:

"Product.Category.SubCategory,Product.Category.SubAnything"

and pass it to Entity Framework:

public Task<TEntity> Get(TKey id, string include)
{
    var query = Context.Set<TEntity>();
    if (!string.IsNullOrEmpty(include))
    {
        query = include.Split(',', StringSplitOptions.RemoveEmptyEntries)
                       .Aggregate(query, (q, p) => q.Include(p));
    }
    return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}
ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
ceferrari
  • 1,597
  • 1
  • 20
  • 25