8

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

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

为了说明这个问题,并作为解决方案的尝试,我formatResponseapollo-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}`)
);

为了便于参考,这些文件可在此 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. 有没有更规范的方法来对深度嵌套的属性进行过滤,以及排序和分页?
4

1 回答 1

5

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

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

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

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

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

于 2017-12-21T10:43:22.660 回答