Руководство для программистов, использующих REST-фул

Этот пост предназначен для хакеров REST, которые хотят перейти на GraphQL. Я объясню, что такое GraphQL, каковы основные концепции, с примерами по ходу дела!

Что такое GraphQL?

GraphQL - это язык запросов для вашего API.

В традиционной архитектуре REST у вас будет конечная точка для каждого типа данных, которые вы хотите обслуживать.

/players для данных игрока. /matches для данных совпадений. Каждая конечная точка REST вызывает функцию, которая извлекает данные, связанные с конечной точкой.

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

Разработчики GraphQL подумали, что мы можем передавать аргументы в полезной нагрузке каждой конечной точке, которая в конечном итоге запрашивает базу данных.

Почему бы не передать конечной точке весь запрос?

Таким образом, мы можем обойтись без использования одной конечной точки для всего приложения. 🔥

Как выглядит запрос?

Рад, что ты спросил. Вот пример запроса на совпадение.

query allMatches {
  matches {
    playerA {
      name
    }
    playerB {
      name
    }
    scoreA
    scoreB
}

Если бы у нас было только одно совпадение, это могло бы вернуть JSON, например:

[
  { playerA: { name: "Bob" },
    playerB: { name: "Bill" },
    scoreA: 7,
    scoreB: 5,
  }
]

Каждое свойство, возвращаемое нашим GraphQL API, было запрошено в запросе. Не хотите определенного поля? Просто опустите это. Магия.

Как запрос становится данными?

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

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

GraphQL не заставляет вас использовать SQL, NoSQL и т. Д. Он не зависит от базовой модели данных. Вы можете вернуть любые данные, к которым имеют доступ и которые могут вернуть ваши распознаватели. Вам просто нужно определить преобразователи для всего, что является частью вашей схемы GraphQL. Это то, что определяет вашу модель данных GraphQL. Итак, теперь у вас есть две схемы:

  1. Для базовой модели данных и
  2. Для GraphQL, чтобы он знал, какие запросы разрешены, какие преобразователи необходимо определить и какие данные должны возвращать преобразователи.

Если данные, возвращаемые преобразователями, не соответствуют схеме, ваши запросы не будут выполнены.

Какая схема?

Давайте покажем вам, как наша схема GraphQL будет выглядеть на примере совпадений, вместе с необходимыми преобразователями.

type Player {
  id: ID!
  name: String
  friends: [Player!]
  wins: Int!
}
type Match {
  id: ID!
  playerA: Player!
  playerB: Player!
  scoreA: Int!
  scoreB: Int!
  playerAwin: Boolean!
}
type Query { 
  matches: [Match!]!
  players: [Player!]!
  player(playerId: ID!): Player
}

Это простая схема для игры в пинг-понг.

Мы определяем наши запросы верхнего уровня под специальным корневым типом, который называется Запрос. Запросы в разделе "Запрос" заменят ваши традиционные конечные точки запроса GET, которые используются для извлечения данных без побочных эффектов.

Есть еще два специальных корневых типа: Мутация и Подписка. Мутации похожи на ваши традиционные запросы POST, используемые для изменения данных. Подписки - это особый способ передачи данных с сервера клиентам, которые решают прослушивать эти события в реальном времени.

Для каждого запроса matches, players, player мы определяем возвращаемые типы.

Для каждого типа объекта Player, Match мы определяем типы полей.

В нашем примере мы используем ID, Int, String, которые все являются поддерживаемыми скалярными типами GraphQL. Есть и другие, которые можно найти здесь. Есть некоторые дополнения, которые мы можем добавить к каждому типу, чтобы сформировать новые типы:

  • Type! ⇒ Восклицательный знак после делает его непустым.
  • [Type] ⇒ Квадратные скобки обозначают список
  • [Type!]! ⇒ ненулевой список типа, каждый из которых также не равен нулю.

Покажи мне резолверы!

Вот пример преобразователя псевдокода в JavaScript для Apollo. Match, Player - это наша абстракция для вашего источника данных.

const resolvers = {
  Query: {
    matches: (parent, args, ctx) => Match.getMatches(),
    players: (parent, args, ctx) => Player.getPlayers(),
    player: (parent, { playerId }, ctx) => Player.getPlayer(playerId)
  },
  Player, {
    wins: ({ id }, args, ctx) => len(Match.getMatches(winner=id))
  },
  Match: {
    playerAwin: ({ scoreA, scoreB }, args, ctx) => scoreA > scoreB
  }
}

Здесь мы определяем преобразователи для трех запросов и для определенных полей двух типов объектов.

Вы должны определить преобразователи для всех запросов, изменений и подписок. Вы также можете определить преобразователи для полей таких типов, как Match или Player выше. Если вы не определяете преобразователь для поля, обычно по умолчанию используется преобразование в свойство родительского объекта с тем же именем. Для Аполлона это так:

Аргументы для распознавателя следующие:

  • parent ⇒ объект с родительскими полями. Это позволяет использовать вложенные запросы GraphQL и придает схеме структуру «граф».
  • args ⇒ аргументы, переданные запросу
  • ctx ⇒ объект с внедренным контекстом, который разделяет состояние операции (например, auth) или может быть внедрен с моделями, которые вы используете для взаимодействия с базовыми источниками данных. Это может быть просто Mongoose ORM или API вашего собственного источника данных.
  • info ⇒ 4-й аргумент, не показанный выше, содержащий информацию о входящем запросе GraphQL

Давайте рассмотрим пример использования преобразователя совпадений выше.

Вот пример запроса для всех совпадений:

query allMatches {
  matches {
    id
    playerA {
      id
      name
      friends
      wins
    }
    playerB {
      id
      name
      friends
      wins
    }
    scoreA
    scoreB
    playerAwin
  }
}

Это вызовет преобразователь совпадений, который, в свою очередь, вызовет Match.getMatches()

Допустим, Match.getMatches() возвращает следующий объект:

[{ id: "randomID", 
   playerA: { id: "randomA", name: "Bob", friends: [] },
   playerB: { id: "randomB", name: "Bob", friends: [] },
   scoreA: 7,
   scoreB: 5,
}]

Но это не все поля, которые мы запрашивали ...

JSON, возвращаемый GraphQL для запроса allMatches выше, на самом деле будет:

[{ id: "randomID", 
   playerA: { id: "randomA", name: "Bob", friends: [], wins: 1 },
   playerB: { id: "randomB", name: "Bob", friends: [], wins: 0 },
   scoreA: 7,
   scoreB: 5,
   playerAwin: true
}]

Итак, откуда взялись недостающие поля? Наши резолверы!

Поскольку мы определили наш возвращаемый тип для запроса matches как [Match!]!, GraphQL вошел туда и применил преобразователи Match для каждого поля! Это разрешило поле playerAwin.

Поскольку объект Matches использует вложенный объект Player, GraphQL применил преобразователи Player к объектам в полях playerA и playerB, которые затем дали нам поля wins.

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

Вредоносные запросы

Давайте взглянем на более интересный запрос, который действительно подчеркивает эту рекурсивную природу:

query recursive {
  matches {
    playerA {
      friends {
        friends {
          friends {
            name
          }
        }
      }
    }
  }
}

Это может произойти, если у вас есть круговая ссылка. В нашем случае Player имеет поле с именем friends, которое представляет собой список Player.

Скажем, наибольшее количество друзей у игрока n, и запрос сделан глубиной d. В худшем случае эта операция может вызвать Player преобразователи O (n ^ d) раз.

На практике, чтобы заблокировать подобные вредоносные запросы, мы можем сделать две вещи:

  • Ограничение глубины. Проверьте на стороне клиента, каким должен быть самый глубокий запрос. Если мы используем предел глубины 10, тогда наша новая сложность наихудшего случая будет O (n ^ 10), что намного лучше, чем экспоненциальный рост по глубине.
  • Ограничение размера пагинации. Скажем, мы подружились с запросом с разбивкой на страницы с аргументами pageSize и cursor. Если бы мы ограничили размер страницы, мы могли бы эффективно сократить количество друзей, которые появляются одновременно. Если мы сделаем ограничение страницы 10 в сочетании с ограничением глубины, сложность наихудшего случая станет O (10 ^ 10) = O (1). 🎉

В своем следующем посте я хотел бы обсудить, как оценить, подходит ли GraphQL, о преимуществах GraphQL перед REST и некоторых популярных технологиях, которые помогут вам ускорить работу.