Запросы GraphQL с таблицами объединяются
Я изучаю GraphQL
, поэтому я создал небольшой проект. Скажем, у меня есть 2 модели, User
и 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,
});
Отношения 1: многие, где пользователь может иметь много комментариев.
Я построил схему следующим образом:
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 запросом?
Ответы
Ответ 1
Концепция, на которую вы ссылаетесь, называется пакетной. Есть несколько библиотек, которые предлагают это. Например:
-
Dataloader: универсальная утилита, поддерживаемая Facebook, которая обеспечивает "совместимый API-интерфейс по различным бэкендам и уменьшает запросы к этим бэкэндам через докетирование и кеширование"
-
join-monster: "Уровень выполнения запросов GraphQL-to-SQL для извлечения пакетных данных".
Ответ 2
Для тех, кто использует .NET и пакет GraphQL для .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
}
}
}
}
}
Мы можем создать переменную в методе "resol" поля:
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));
}