【问题标题】:GraphQL: Filtering, sorting and paging on nested entities from separate data sources?GraphQL:对来自不同数据源的嵌套实体进行过滤、排序和分页?
【发布时间】:2017-03-06 11:16:40
【问题描述】:

我正在尝试使用 graphql 将多个休息端点联系在一起,但我一直在思考如何过滤、排序和分页生成的数据。具体来说,我需要按嵌套值过滤和/或排序。

我无法在所有情况下都对其余端点进行过滤,因为它们是具有独立数据库的独立微服务。 (即,我可以在文章的其余端点中过滤 title,但不能在 author.name 上过滤)。排序也是如此。而且没有过滤和排序,其余端点也无法进行分页。

为了说明问题并尝试解决问题,我在apollo-server 中使用formatResponse 提出了以下建议,但我想知道是否有更好的方法。

我已将解决方案归结为我能想到的最小文件集:

data.js 表示 2 个虚构的休息端点将返回的内容:

export const Authors = [{ id: 1, name: 'Sam' }, { id: 2, name: 'Pat' }];

export const Articles = [
  { id: 1, title: 'Aardvarks', author: 1 },
  { id: 2, title: 'Emus', author: 2 },
  { id: 3, title: 'Tapir', author: 1 },
]

架构定义为:

import _ from 'lodash';
import {
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLList,
  GraphQLString,
  GraphQLInt,
} from 'graphql';

import {
  Articles,
  Authors,
} from './data';

const AuthorType = new GraphQLObjectType({
  name: 'Author',
  fields: {
    id: {
      type: GraphQLInt,
    },
    name: {
      type: GraphQLString,
    }
  }
});

const ArticleType = new GraphQLObjectType({
  name: 'Article',
  fields: {
    id: {
      type: GraphQLInt,
    },
    title: {
      type: GraphQLString,
    },
    author: {
      type: AuthorType,
      resolve(article) {
        return _.find(Authors, { id: article.author })
      },
    }
  }
});

const RootType = new GraphQLObjectType({
  name: 'Root',
  fields: {
    articles: {
      type: new GraphQLList(ArticleType),
      resolve() {
        return Articles;
      },
    }
  }
});

export default new GraphQLSchema({
  query: RootType,
});

而主要的 index.js 是:

import express from 'express';
import { apolloExpress, graphiqlExpress } from 'apollo-server';
var bodyParser = require('body-parser');
import _ from 'lodash';
import rql from 'rql/query';
import rqlJS from 'rql/js-array';

import schema from './schema';
const PORT = 8888;

var app = express();

function formatResponse(response, { variables }) {
  let data = response.data.articles;

  // Filter
  if ({}.hasOwnProperty.call(variables, 'q')) {
    // As an example, use a resource query lib like https://github.com/persvr/rql to do easy filtering
    // in production this would have to be tightened up alot
    data = rqlJS.query(rql.Query(variables.q), {}, data);
  }

  // Sort
  if ({}.hasOwnProperty.call(variables, 'sort')) {
    const sortKey = _.trimStart(variables.sort, '-');
    data = _.sortBy(data, (element) => _.at(element, sortKey));
    if (variables.sort.charAt(0) === '-') _.reverse(data);
  }

  // Pagination
  if ({}.hasOwnProperty.call(variables, 'offset') && variables.offset > 0) {
    data = _.slice(data, variables.offset);
  }
  if ({}.hasOwnProperty.call(variables, 'limit') && variables.limit > 0) {
    data = _.slice(data, 0, variables.limit);
  }

  return _.assign({}, response, { data: { articles: data }});
}

app.use('/graphql', bodyParser.json(), apolloExpress((req) => {
  return {
    schema,
    formatResponse,
  };
}));

app.use('/graphiql', graphiqlExpress({
  endpointURL: '/graphql',
}));

app.listen(
  PORT,
  () => console.log(`GraphQL Server running at http://localhost:${PORT}`)
);

为便于参考,这些文件可在this gist 获得。

通过这个设置,我可以发送这个查询:

{
  articles {
    id
    title
    author {
      id
      name
    }
  } 
}

连同这些变量(看起来这不是变量的预期用途,但这是我可以将后处理参数放入 formatResponse 函数的唯一方法。):

{ "q": "author/name=Sam", "sort": "-id", "offset": 1, "limit": 1 }

并获取此响应,过滤到 Sam 是作者的位置,按 id 降序排序,并获取页面大小为 1 的第二页。

{
  "data": {
    "articles": [
      {
        "id": 1,
        "title": "Aardvarks",
        "author": {
          "id": 1,
          "name": "Sam"
        }
      }
    ]
  }
}

或者这些变量:

{ "sort": "-author.name", "offset": 1 }

对于这个回复,按作者姓名降序排序,得到除第一篇以外的所有文章。

{
  "data": {
    "articles": [
      {
        "id": 1,
        "title": "Aardvarks",
        "author": {
          "id": 1,
          "name": "Sam"
        }
      },
      {
        "id": 2,
        "title": "Emus",
        "author": {
          "id": 2,
          "name": "Pat"
        }
      }
    ]
  }
}

所以,如您所见,我使用 formatResponse 函数进行后处理来进行过滤/分页/排序。 .

所以,我的问题是:

  1. 这是一个有效的用例吗?
  2. 除了排序和分页之外,还有更规范的方法来对深度嵌套的属性进行过滤吗?

【问题讨论】:

  • 我知道您没有使用中继,但您是否阅读过有关中继connections 的信息?我相信它将帮助您了解如何请求分页集合。现在,关于如何在您的架构中过滤和分页(我自己手头有一个类似的),我相信您唯一的解决方案是在某个地方与您的数据相交。以您的示例为例,如果您想按 author.name 过滤,您必须首先搜索具有该名称的作者,然后然后搜索具有这些作者的文章。
  • 还没有使用GraphQL,但是在考虑分页的情况下对该主题进行了一些研究,我偶然发现了这篇文章Understanding Pagination REST GraphQL and Relay,其中谈到了即将推出的分页功能。这可能有助于回答您的相关问题。
  • 以 author.name 开头的问题在于,您假设作者类型将从可以有效排序的单一来源解析。但是在分片环境中,我们可能有两个或多个底层数据源需要两个独立的查询,最终都得到作者结果。据我所知,进行此处讨论的那种复杂排序的唯一通用方法是使用过滤器过程,该过程明确设计用于对 graphql 结果进行排序。

标签: microservices graphql graphql-js apollo-server


【解决方案1】:

这是一个有效的用例吗?是否有更规范的方法来对深度嵌套的属性进行过滤以及排序和分页?

原始任务的主要部分在于将不同数据库上的集合隔离在不同的微服务上。事实上,对某个键进行集合加入和后续过滤是必要的,但直接不可能,因为原始集合中没有字段可以过滤、排序或分页。

简单的解决方案是对原始集合执行完整或过滤查询,然后在应用服务器上执行连接和过滤结果数据集,例如通过 lodash,例如您的解决方案。 In 对于小型集合是可能的,但在一般情况下会导致大量数据传输和排序效率低下,因为没有索引结构 - 真正的 RB-tree 或 SkipList,因此二次复杂度不是很好。

依赖于应用服务器上的资源量,可以在那里建立特殊的缓存和索引表。如果集合结构是固定的,集合条目与其字段之间的一些关系可以反映在特殊的搜索表中,并随时更新。这就像查找和搜索索引创建,但不是数据库,而是在应用程序服务器上。当然,它会消耗资源,但会比直接类似 lodash 的排序更快。

如果可以访问原始数据库的结构,也可以从另一个方面解决任务。关键是非规范化。与经典关系方法相反,集合可以具有重复信息,以避免进一步的连接操作。例如,Articles 集合可以有一些来自 Authors 集合的信息,这些信息是在后续操作中进行过滤、排序和分页所必需的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-11-11
    • 2017-03-24
    • 1970-01-01
    • 2018-07-21
    • 2019-04-09
    • 2021-02-10
    • 2015-12-12
    • 1970-01-01
    相关资源
    最近更新 更多