GraphQL queries with tables join

2020-05-20 07:28发布

问题:

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 1: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?

回答1:

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."



回答2:

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));
}