使用Node.js进行表联接的GraphQL查询

35

我正在学习 GraphQL 所以我建了一个小项目。假设我有2个模型:UserComment

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,
});
关系是“一对多”,一个用户可以有很多评论。
我已经按照以下模式构建了模式:
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()
    }
  })
});

查询语句:

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

执行用户及其评论的查询可以通过1个请求完成,如下所示:

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

据我理解,客户端将使用1个查询获取结果,这是使用 GraphQL的好处。但服务器将执行2个查询,一个用于用户,另一个用于他的评论。

我的问题是,构建 GraphQL模式和类型以及在表之间组合联接的最佳实践是什么,以便服务器也可以使用1个请求执行查询?

2个回答

16
您所提到的概念被称为批处理。有几个库可以提供这个功能,例如:
  • Dataloader:由Facebook维护的通用工具,提供“在各种后端上提供一致的API,并通过批处理和缓存减少对这些后端的请求”的功能。

  • join-monster:“用于批量获取数据的GraphQL-to-SQL查询执行层。”


如果您更喜欢视频,这里有一个关于Join Monster的演讲:https://www.youtube.com/watch?v=Y7AdMIuXOgs :) - marktani
谢谢您的帖子。您会选择哪一个呢?一方面,我有Facebook和1800颗星的库,可以批量处理一些请求,但另一方面,我也有一个将所有内容批量处理为1个请求的库。 - itaied
我们在 Graphcool 使用 Dataloader 取得了很好的体验,那时我们正在使用 graphql-js 运行 node GraphQL 后端。对于 join-monster 我们不能分享任何经验。我认为它们在功能和用法上略有不同,所以你需要自己测试一下 :) - marktani
对我来说,批处理是一种不同的东西!批处理意味着获取一个ID数组(就像1 + N场景中一样),并使用一个查询将它们全部获取。但是,Join Monster和Photonjs所做的事情更好,它们利用SQL连接仅使用单个SQL查询来满足GQL查询。 - vanthome

6

如果你正在使用.NET和GraphQL for .NET包,我已经创建了一个扩展方法,将GraphQL查询转换为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;
            }
        }}}

假设我们有以下查询:
query {
  getHistory {
    id
    product {
      id
      category {
        id
        subCategory {
          id
        }
        subAnything {
          id
        }
      }
    }
  }
}

我们可以在字段的“resolve”方法中创建一个变量:

var include = context.GetIncludeString();

生成以下字符串:

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

并将其传递给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));
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接