GraphQL: фильтрация, сортировка и подкачка по вложенным объектам из отдельных источников данных?

#microservices #graphql #graphql-js #apollo-server

#микросервисы #graphql #graphql-js #apollo-сервер

Вопрос:

Я пытаюсь использовать graphql для объединения нескольких конечных точек rest, и я застрял на том, как фильтровать, сортировать и разбивать на страницы результирующие данные. В частности, мне нужно отфильтровать и / или отсортировать по вложенным значениям.

Я не могу выполнить фильтрацию на конечных точках rest во всех случаях, потому что это отдельные микросервисы с отдельными базами данных. (т. Е. я мог бы выполнять фильтрацию на title в конечной точке rest для статей, но не на author.name ). Аналогично с сортировкой. А без фильтрации и сортировки разбивка на страницы также не может быть выполнена на конечных точках rest.

Чтобы проиллюстрировать проблему и в качестве попытки решения, я придумал следующее, используя formatResponse в apollo-server, но мне интересно, есть ли способ получше.

Я свел решение к самому минимальному набору файлов, который только мог придумать:

data.js представляет то, что было бы возвращено двумя вымышленными конечными точками rest:

 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') amp;amp; variables.offset > 0) {
    data = _.slice(data, variables.offset);
  }
  if ({}.hasOwnProperty.call(variables, 'limit') amp;amp; 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}`)
);
  

For ease of reference, these files are available at this gist.

With this setup, I can send this query:

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

Along with these variables (It seems like this is not the intended use for the variables, but it was the only way I could get the post processing parameters into the formatResponse function.):

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

и получите этот ответ, отфильтрованный до того, где Сэм является автором, отсортированный по убыванию идентификатора и получающий получение второй страницы, где размер страницы равен 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. Есть ли более канонический способ выполнить фильтрацию по глубоко вложенным свойствам наряду с сортировкой и разбиением на страницы?

Комментарии:

1. Я знаю, что вы не используете relay, но читали ли вы о соединениях relay? Я полагаю, что это поможет вам понять, как запрашивать коллекции с разбивкой на страницы. Теперь, что касается того, как фильтровать и разбивать на страницы в вашей архитектуре (у меня у самого есть похожий), я считаю, что ваше единственное решение — где-то пересекать ваши данные. Исходя из вашего примера, если вы хотите выполнить фильтрацию по author.name , вам придется сначала выполнить поиск авторов с этим именем, а затем выполнить поиск статей с этими авторами.

2. GraphQL Еще не использую, но провожу некоторое исследование по этому вопросу с учетом разбивки на страницы, и я наткнулся на эту статью Understanding Pagination REST GraphQL and Relay , в которой рассказывается о предстоящей функции разбивки на страницы. Это может быть полезно для ответа на ваши вопросы по этому поводу.

3. Проблема с запуском с author.name вы предполагаете, что тип автора будет разрешен из одного источника, который может быть эффективно отсортирован. Но в разделенной среде у нас может быть два или более базовых источника данных, требующих двух изолированных запросов, оба из которых заканчиваются авторскими результатами. Насколько я могу судить, единственный универсальный способ выполнить описанную здесь сложную сортировку — это процесс фильтрации, который явно предназначен для сортировки результатов graphql.

Ответ №1:

Является ли это допустимым вариантом использования? Есть ли более канонический способ выполнить фильтрацию по глубоко вложенным свойствам наряду с сортировкой и разбиением на страницы?

Основная часть первоначального поиска заключается в разделении коллекций из разных баз данных на отдельных микросервисах. На самом деле, необходимо выполнить объединение коллекции и последующую фильтрацию по некоторому ключу, но это напрямую невозможно, поскольку в исходной коллекции нет поля для фильтрации, сортировки или разбиения на страницы.

Простым решением является выполнение полных или отфильтрованных запросов к исходным коллекциям, а затем объединение и фильтрация результирующего набора данных на сервере приложений, например Lodash, например, в вашем решении. Это возможно для небольших коллекций, но в общем случае приводит к большой передаче данных и неэффективной сортировке, поскольку нет структуры индекса — реального RB-дерева или списка пропусков, поэтому с квадратичной сложностью это не очень хорошо.

В зависимости от объема ресурсов на сервере приложений там могут быть созданы специальные таблицы кэша и индексов. Если структура коллекции исправлена, некоторые связи между записями коллекции и их полями могут быть отражены в специальной таблице поиска и обновлены соответственно на demain. Это похоже на создание индекса поиска, но не в базе данных, а на сервере приложений. Конечно, это будет потреблять ресурсы, но будет быстрее, чем прямая сортировка Lodash-подобным образом.

Также задача может быть решена с другой стороны, если есть доступ к структуре исходных баз данных. Ключ — это денормализация. В отличие от классического подхода к связям, коллекции могут содержать дублирующую информацию для кодирования дальнейшей операции объединения. Например, коллекция статей может содержать некоторую информацию из коллекции авторов, которая необходима для выполнения фильтрации, сортировки и разбивки на страницы в дальнейших операциях.