Я помню время, когда GraphQL стал популярным, в то время я работал руководителем команды Android и BackEnd. Тогда мы много думали об использовании GraphQL, но в итоге решили использовать REST, потому что GraphQL был действительно новым для всех, и у нас не было времени на рискованные разработки. Второй раз, когда я столкнулся с GraphQL, я столкнулся с улучшением движка CMS в Natural Intelligence, который используется и модифицируется отделами исследований и разработок компании. На этот раз выбор GraphQL был естественным решением. .
В этой статье я хочу рассказать, как мы построили инфраструктуру GraphQL и как мы используем GraphQL в производственной среде.

Во-первых, почему мы решили использовать GraphQL?

  • Чрезмерная выборка: мы увидели, что при использовании REST для визуализации пользовательского интерфейса для нашего пользователя мы извлекаем много бесполезных данных, что замедляет запросы и утяжеляет все кластерные сети. , мы хотели получить только необходимые данные для пользовательского интерфейса.
  • Схема: при использовании стандартного REST вы толком не знаете, что будет результатом вызова, конечно, вы можете посмотреть схему базы данных или чванство, но во многих случаях у вас есть мутации данных в качестве крючков предварительного ответа и не имеют чванства. При написании запросов GraphQL разработчик должен знать о схеме, и если что-то не существует в схеме, разработчик немедленно получит ошибку проверки.
import { gql } from '@apollo/client';
import { omitBy, isEmpty } from 'lodash';
export const query = gql`
  query Sites($queryParams: JSONObject, $count: Int, $offset: Int) {
   sites(queryParams: $queryParams, count: $count, offset: $offset){
      id
      siteId
      createdAt
      createdByUser
      title
      statistics {
        isCurrentPublished
        wasPublished
        totalVariants
        defaultPublishedVariant {
          createdByUser
          createdAt
          publishAt
        }
      }
    }
  }
`;
export const getVariables = props => ({
  queryParams: omitBy(props.query, isEmpty),
  count: 50,
  offset: 0,
});
export default query;
  • Параллелизм: в большинстве случаев мы наблюдали параллельные и поставленные в очередь запросы, исходящие от клиентского приложения, после ответа мы агрегировали возвращенные данные в логическую структуру пользовательского интерфейса. Мы хотели сократить процесс принятия решений на стороне клиента и перенести всю старую логику запросов на сторону сервера. Мы решили взять нестандартное решение и использовать apollo gateway. С помощью шлюза apollo теперь мы можем сделать только один вызов на стороне сервера и позволить шлюзу решить, какие микросервисы (преобразователи) вызывать для выполнения запроса, сделать его параллельным или поставленным в очередь, и агрегировать данные, используя наш первоначальный запрос.

  • Безопасность схемы: мы хотели, чтобы наши схемы были безопасными и всегда имели обратную совместимость для недостаточно используемых полей. Для этого мы добавили механизм проверки аполлона на уровне PR для каждого микросервиса, и теперь, если кто-то удалил или изменил используемое поле, процесс проверки аполлона завершится ошибкой, и вы не сможете развернуть свое изменение ( если ваше изменение безопасно, администратор студии Apollo может пометить это изменение как безопасное и позволить процессу двигаться дальше). После того, как изменение помечено как безопасное, в рамках процесса развертывания мы выполняем процесс публикации apollo, который загружает новую схему в студию apollo.

  • Площадка GraphQL: мы хотели, чтобы команды могли легко работать с нашими данными, не думая о REST, и легко видеть все схемы в одном месте, для этого мы интегрировали игровую площадку GraphQL как часть нашего приложения, и сделали его доступным для наших соответствующих внутренних пользователей.

Итак, как мы это реализовали?

У нас много похожих потоков, и мы полагаемся на стабильные уровни архитектуры контроллера, сервиса и репозитория (подробнее об этом можно прочитать здесь). Мы решили создать пакет, чтобы включить в него общие преобразователи, определения и построители запросов, которые будут использоваться в каждом микросервисе. Это решение может быть расширено для поддержки любого конкретного варианта использования. Идея состоит в том, что каждый строитель будет иметь внедряемый уровень службы и параметры параметров, которые будут использоваться для разрешения данных.

Пример универсальных преобразователей:

import queryParamsType from './types/queryParams.interface';
import Service from './types/service.interface';
export interface ResolverDTO {
  service: Service;
}
export interface ResolverWithParamDTO extends ResolverDTO {
  paramName: string;
}
export const getEntities =
  ({ service }: ResolverDTO) =>
  async (parent: object, args: queryParamsType) => {
    const result = await service.getEntities(args);
    return result.content;
  };
export const getById =
  ({ service }: ResolverDTO) =>
  async (parent: object, args: queryParamsType) => {
    const { id } = args;
    const result = await service.getEntity(id);
    return result.content;
  };
export const getExternalsById =
  ({ service, paramName }: ResolverWithParamDTO) =>
  async (parent: object) => {
    const parentIdParam = parent[paramName];
    const result = await service.getEntity(parentIdParam);
    return result.content;
  };
export default {
  getEntities,
  getById,
  getExternalsById,
};

Общие компоновщики определений:

import niPluralize from '@naturalintelligence/pluralize';
import Service from './types/service.interface';
import genericResolvers from './genericResolvers';
export interface DefinitionsDTO {
  entityName: string;
  service: Service;
}
export const getEntities = ({ entityName, service }: DefinitionsDTO): object => ({
  [`${niPluralize(entityName)}`]: genericResolvers.getEntities({ service }),
});
export const getById = ({ entityName, service }: DefinitionsDTO): object => ({
  [`${entityName}ById`]: genericResolvers.getById({ service }),
});
export const getDefault = ({ entityName, service }: DefinitionsDTO): object => ({
  ...getEntities({ entityName, service }),
  ...getById({ entityName, service }),
});
export default = getDefault;

Реализация преобразователей в микросервисе:

const {
  backOffice: {
    resolvers: { genericResolvers, definitions },
  },
} = require('@naturalintelligence/shared-graphql');
const productsService = require('../services/products-service');
const sitesService = require('../services/sites-service');
const commonEntityResolvers = {
  site: genericResolvers.getExternalsById({
    service: sitesService,
    paramName: 'siteId',
  }),
};
const resolvers = {
  Product: commonEntityResolvers,
  Query: {
    ...definitions.getDefault({ entityName: 'product', service: productsService })
  },
};
module.exports = {
  resolvers,
};

Общие определения типов в общем пакете:

import { upperFirst } from 'lodash';
import niPluralize from '@naturalintelligence/pluralize';
export interface QueryDTO {
  entityName: string;
  returnableType?: string;
}
export const getEntitiesQuery = ({ entityName, returnableType = upperFirst(entityName) }: QueryDTO): string => `
  ${niPluralize(
    entityName
  )}(queryParams: JSONObject, extendedInfo: Int, count: Int, offset: Int, search: String): [${returnableType}]
  `;
export const getByIdQuery = ({ entityName, returnableType = upperFirst(entityName) }: QueryDTO): string => `
  ${entityName}ById(id: ID!): ${returnableType}
    `;
export const getEntityDefaultQueries = ({ entityName, returnableType = upperFirst(entityName) }: QueryDTO): string => `
    ${getEntitiesQuery({ entityName, returnableType })}
    ${getByIdQuery({ entityName, returnableType })}
  `;
export default getEntityDefaultQueries;

Реализация Typedefs в микросервисе:

const { gql } = require('apollo-server-express');
const {
  backOffice: {
    queryTypes,
    typeDefs: { Statistics, Scalars },
  },
} = require('@naturalintelligence/shared-graphql');
const { ProductSchema } = require('./schemas');
const typeDefs = gql`
    ${Scalars}
    ${Statistics}
    ${ProductSchema}
    extend type Query {
            ${queryTypes.getEntityDefaultQueries({ entityName: 'product' })}
      }
    `;
module.exports = {
  typeDefs,
};

Итак, каковы результаты?

  • Размер полезной нагрузки резко уменьшился.
  • Количество клиентских запросов уменьшилось.

  • Скорость разработчиков увеличилась.
  • Повышена безопасность схемы.

Выводы

Когда я впервые столкнулся с GraphQL, я боялся его использовать, потому что это не было стандартом, это была новая технология в то время. Но через несколько лет стало ясно, что выбор GraphQL — это способ улучшить нашу систему.

Следуя шагам, описанным в этой статье, вы сможете создать свою собственную инфраструктуру, как и я, чтобы повысить скорость разработки, помочь разработчикам добавлять новые объекты в GraphQL и повысить качество вашей системы.

«Хорошая архитектура является ключом к созданию модульных, масштабируемых, поддерживаемых и тестируемых приложений».

Большое спасибо компании Natural Intelligence, которая предоставила мне место и все ресурсы для реализации успешного проекта по улучшению.

Удачного кодирования :)