web-dev-qa-db-fra.com

Requêtes GraphQL avec jointure de tables

J'apprends GraphQL alors j'ai construit un petit projet. Disons que j'ai 2 modèles, User et 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,

});

Les relations sont 1: plusieurs, où un utilisateur peut avoir de nombreux commentaires.
J'ai construit le schéma comme ceci:

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

Et la requête:

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

L'exécution de la requête pour un utilisateur et ses commentaires se fait avec 1 requête, comme ceci:

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

Si je comprends bien, le client obtiendra les résultats en utilisant 1 requête, c'est l'avantage en utilisant GraphQL. Mais le serveur exécutera 2 requêtes, une pour l'utilisateur et une autre pour ses commentaires.
Ma question est, quelles sont les meilleures pratiques pour construire le schéma et les types GraphQL et combiner la jointure entre les tables, afin que le serveur puisse également exécuter la requête avec 1 requête?

24
itaied

Le concept auquel vous faites référence est appelé batching. Il existe plusieurs bibliothèques qui offrent cela. Par exemple:

  • Dataloader : utilitaire générique maintenu par Facebook qui fournit "une API cohérente sur divers backends et réduit les requêtes vers ces backends via le batching et la mise en cache"

  • join-monster : "Une couche d'exécution de requête GraphQL-to-SQL pour l'extraction de données par lots."

12
marktani

Pour tous ceux qui utilisent .NET et le package GraphQL pour .NET , j'ai créé une méthode d'extension qui convertit la requête GraphQL en Entity Framework comprend.

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

Disons que nous avons la requête suivante:

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

On peut créer une variable dans la méthode "résoudre" du champ:

var include = context.GetIncludeString();

qui génère la chaîne suivante:

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

et le transmettre à 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));
}
5
ceferrari