GraphQL:对来自不同数据源的嵌套实体进行过滤,排序和分页?

我正在尝试使用graphql将许多休息端点绑定在一起,而我仍然坚持如何过滤,排序和分页结果数据。具体来说,我需要通过嵌套值进行过滤和/或排序。

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

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

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

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函数进行后期处理以进行过滤/分页/排序。 。

所以,我的问题是:

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

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

原始任务的主要部分在于在不同的微服务上分离不同数据库上的集合。实际上,对某些键执行集合连接和后续过滤是必要的,但这是不可能的,因为原始集合中没有字段可用于过滤,排序或分页。

明确的解决方案是对原始集合执行完整或过滤查询,然后在应用程序服务器上执行连接和过滤结果数据集,例如,通过lodash,你的解决方案。对于小型集合是可能的,但是一般情况下会导致大量数据传输和无法排序,因为没有索引结构 - 真正的RB树或SkipList,所以二次复杂性并不是很好。

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

如果可以访问原始数据库的结构,也可以从另一方解决任务。关键是非规范化。在经典关系方法的计数器中,集合可以具有用于避免进一步连接操作的公开信息。例如,文章集合可以从作者集合中获得一些信息,这是在进一步操作中执行过滤,排序和分页的必要条件。