Save 37% off PRO during our Black Friday Sale! »

Как мыслить графами или почему GraphQL — это не просто представление структуры БД

Как мыслить графами или почему GraphQL — это не просто представление структуры БД

Доклад предназначен для тех, кто еще не разрабатывал свои API на GraphQL, и для тех, кто уже попробовал и не увидел особой разницы с REST.
Мы определимся с тем, что такое GraphQL, поговорим о его философии, и попробуем ответить на следующие вопросы:
как пользоваться языком запросов?
что можно сделать с помощью GraphQL?
что нельзя сделать с помощью GraphQL?
что такое схема и зачем она нужна?
как реализовать GraphQL API?
какие паттерны проектирования схемы API существуют?

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

August 02, 2019
Tweet

Transcript

  1. КАК МЫСЛИТЬ ГРАФАМИ или Почему GraphQL - это не просто

    представление структуры БД Дмитрий Цепелев Злые Марсиане
  2. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 2

  3. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 3 evilmartians.com

  4. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 4 evilmartians.com

  5. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev REST GET /users GET /users/:id

    POST /users PATCH /users/:id DELETE /users/:id 5 • ресурсы определяются с помощью URL • действия определяются через HTTP verbs • ассоциации выражены через IDs
  6. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 6 REST • ресурсы определяются

    с помощью URL • действия определяются через HTTP verbs • ассоциации выражены через IDs GET /users GET /users/:id POST /users PATCH /users/:id DELETE /users/:id
  7. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev GET /users/42 { "id": 42,

    "name": "John Doe", "accountId": 23 } 7 REST • ресурсы определяются с помощью URL • действия определяются через HTTP verbs • ассоциации выражены через IDs
  8. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev POST /users/42/join-community POST /communities/67/join 8

    Как правильно?
  9. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev POST /users/42/join-community POST /communities/67/join 9

    действие определено через URL + HTTP verb Как правильно?
  10. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 10 В REST-стиле POST /community-memberships

  11. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 11 Представление данных в виде

    графа • каждая сущность - тип • каждый тип имеет список полей (field) • некоторые типы связаны
  12. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev • каждая сущность - тип

    • каждый тип имеет список полей (field) • некоторые типы связаны 12 Представление данных в виде графа
  13. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 13 Представление данных в виде

    графа • каждая сущность - тип • каждый тип имеет список полей (field) • некоторые типы связаны
  14. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 14 Представление данных в виде

    графа • каждая сущность - тип • каждый тип имеет список полей (field) • некоторые типы связаны
  15. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 15 GraphQL позволяет получить все

    данные одним запросом
  16. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 16 GET /user GET /repos/:owner/:repo

    GET /repos/:owner/:repo/issues Underfetching в REST
  17. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 17 GET /user { "login":

    "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https:"//github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https:"//api.github.com/users/octocat", "html_url": "https:"//github.com/octocat", "followers_url": "https:"//api.github.com/users/octocat/followers", "following_url": "https:"//api.github.com/users/octocat/following{/ other_user}", "gists_url": "https:"//api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https:"//api.github.com/users/octocat/starred{/owner}{/ repo}", "subscriptions_url": "https:"//api.github.com/users/octocat/subscriptions", "organizations_url": "https:"//api.github.com/users/octocat/orgs", "repos_url": "https:"//api.github.com/users/octocat/repos", "events_url": "https:"//api.github.com/users/octocat/events{/privacy}", "received_events_url": "https:"//api.github.com/users/octocat/ received_events", "type": "User", "site_admin": false, "name": "monalisa octocat", … Overfetching в REST … "company": "GitHub", "blog": "https:"//github.com/blog", "location": "San Francisco", "email": "octocat@github.com", "hireable": false, "bio": "There once was""...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "created_at": "2008-01-14T04:33:35Z", "updated_at": "2008-01-14T04:33:35Z", "private_gists": 81, "total_private_repos": 100, "owned_private_repos": 100, "disk_usage": 10000, "collaborators": 8, "two_factor_authentication": true, "plan": { "name": "Medium", "space": 400, "private_repos": 20, "collaborators": 0 } }
  18. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev Что такое GraphQL? • язык

    запросов • среда исполнения 18
  19. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 19 ⇨ Язык запросов ⇦

    Схема API Реализация API Что может пойти не так Как проектировать схему Эволюция API
  20. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 20 Request: query { currentUserId

    } Response: { "data": { "currentUserId": 42 } } Поля
  21. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 21 Request: query { user(id:

    42) { orders { items { product { title } quantity } } } } Response: { "data": { "user": { "orders": [ { "items": [ { "product": { "title": "iPhone 7" }, "quantity": 2 } ] } ] } } } Аргументы
  22. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 22 Вложенные выборки Request: query

    { user(id: 42) { orders { items { product { title } quantity } } } } Response: { "data": { "user": { "orders": [ { "items": [ { "product": { "title": "iPhone 7" }, "quantity": 2 } ] } ] } } }
  23. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 23 query FetchUser($userId: Int) {

    user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 } Именованные запросы
  24. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 24 Переменные query FetchUser($userId: Int)

    { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  25. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev Транспортный уровень 25 • согласно

    спецификации – transport-agnostic • в реализациях: - HTTP POST - один эндпоинт - код ответа 200 - OK
  26. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 26 Request: query { user(id:

    404) { id name } } Response: { "data": null, "errors": [ { "message": "User not found" } ] } Представление ошибок
  27. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 27 Мутации: обновление данных

  28. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 28 Request: mutation { createPost(

    title: "draft", content: "no content" ) { id } } Response: { "data": { "createPost" { "id": 4 } } } Мутации: обновление данных
  29. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 29 Request: subscription { postCreated

    { id title content } } Response: { "data": { "postCreated": { "id": 4, "title": "draft", "content": "no content" } } } Подписки: получение обновлений
  30. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 30 Язык запросов ⇨ Схема

    API ⇦ Реализация API Что может пойти не так Как проектировать схему Эволюция API
  31. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 31 type UserType { name:

    String! orders(page: Int): [OrderType]! } Типы
  32. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 32 Поля type UserType {

    name: String! orders(page: Int): [OrderType]! }
  33. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 33 Скалярные типы type UserType

    { # Int|Float|Boolean|String|ID name: String! orders(page: Int): [OrderType]! }
  34. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 34 Списки type UserType {

    name: String! orders(page: Int): [OrderType]! }
  35. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 35 Аргументы type UserType {

    name: String! orders(page: Int): [OrderType]! }
  36. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 36 type QueryType { users:

    [UserType]! } type MutationType { signUp(login: String!, password: String!): UserType! } type SubscriptionType { userSignedUp: UserType! } Корневые типы
  37. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 37 Валидация аргументов type QueryType

    { orders(page: Int):[OrderType]! } query { orders(page: "1") { id } } { "errors": [{ "message": "Argument 'page' on Field 'orders' has an invalid value. Expected type 'Int'.", "fields": ["query", "orders", "page"] }] }
  38. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 38 enum UserModerationStatus { MODERATION

    APPROVED REJECTED } type UserType { name: String! moderationStatus: UserModerationStatus! } Перечисления
  39. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 39 scalar DateTime type OrderType

    { placedAt: DateTime! } { "placedAt": "2019-03-01T19:18:37+03:00" } Пользовательские скалярные типы
  40. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 40 Автоматическая документация: GraphiQL github.com/graphql/graphiql

  41. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 41 github.com/2fd/graphdoc Автоматическая документация: GraphDoc

  42. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 42 Язык запросов Схема API

    ⇨ Реализация API ⇦ Что может пойти не так Как проектировать схему Эволюция API
  43. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev class QueryType < GraphQL"::Schema"::Object field

    :users, [UserType], description: "List of all users", null: false def users # SELECT * FROM users; User.all end end QueryType на бэке: объявляем поле 43
  44. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 44 QueryType на бэке: возвращаемый

    тип class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users # SELECT * FROM users; User.all end end
  45. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 45 QueryType на бэке: resolver

    class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users # SELECT * FROM users; User.all end end
  46. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 46 QueryType на бэке: источник

    данных class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users # SELECT * FROM users; User.all end end
  47. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 47 class UserType < GraphQL"::Schema"::Object

    field :name, String, null: false field :orders, [OrderType], null: false do argument :page, Int, required: false end def orders(page: nil) object.orders.page(page) end end Объявление типа на бэке: объявляем поле
  48. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 48 Объявление типа на бэке:

    resolver class UserType < GraphQL"::Schema"::Object field :name, String, null: false field :orders, [OrderType], null: false do argument :page, Int, required: false end def orders(page: nil) object.orders.page(page) end end
  49. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 49 Объявление типа на бэке:

    передаем аргументы class UserType < GraphQL"::Schema"::Object field :name, String, null: false field :orders, [OrderType], null: false do argument :page, Int, required: false end def orders(page: nil) object.orders.page(page) end end
  50. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 50 Язык запросов Схема API

    Реализация API ⇨ Что может пойти не так ⇦ Как проектировать схему Эволюция API
  51. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 51 query { user(id: 42)

    { orders { user { orders { user { … } } } } } } Сложность и глубина запросов
  52. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 52 Request: query { articles

    { id author { name } } } Response: { "data": { "articles" [ { "id": "1", "author": { "name": "John" } }, { "id": "2", "author": { "name": "John" } }, ] } } Дублирование данных в ответе
  53. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 53 type CategoryType { name:

    String! subcategories: [CategoryType]! } query { categories { name subcategories { name subcategories { name } } } } Нет рекурсивных запросов
  54. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 54 Сложно кэшировать ответы •

    REST: ответ зависит от HTTP verb и URL • GraphQL: ответ зависит от selection set • нет легкого и эффективного способа кэшировать ответ GraphQL $
  55. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 55 class QueryType < GraphQL"::Schema"::Object

    field :users, [UserType], null: false def users # SELECT * FROM users; # SELECT * FROM orders where user_id = ?; # SELECT * FROM orders where user_id = ?; # SELECT * FROM orders where user_id = ?; # SELECT * FROM orders where user_id = ?; User.all end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end Проблема N+1
  56. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 56 class QueryType < GraphQL"::Schema"::Object

    field :users, [UserType], null: false def users # SELECT * FROM users; # SELECT * FROM orders where user_id IN (…); User.preload(:orders) end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end Устраняем N+1: preload
  57. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 57 query { user(id: 24)

    { id name } } # SELECT * FROM users; # SELECT * FROM orders where user_id IN (…); Устраняем N+1: preload
  58. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 58 class QueryType < GraphQL"::Schema"::Object

    field :users, [UserType], null: false, extras: [:lookahead] def users(lookahead:) if lookahead.selects?(:orders) User.preload(:orders) else User.all end end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end Устраняем N+1: работаем с запросом
  59. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 59 class QueryType < GraphQL"::Schema"::Object

    field :users, [UserType], null: false, extras: [:lookahead] def users(lookahead:); end end class OrganisationType < GraphQL"::Schema"::Object field :users, [UserType], null: false, extras: [:lookahead] def users(lookahead:) if lookahead.selects?(:orders) object.users.preload(:orders) else object.users end end end Устраняем N+1: работаем с запросом
  60. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 60 Устраняем N+1: batching class

    QueryType < GraphQL"::Schema"::Object field :users, [UserType], null: false def users User.all end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false def orders AssociationLoader.for(User, :orders).load(object) end end
  61. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 61 Язык запросов Схема API

    Реализация API Что может пойти не так ⇨ Как проектировать схему ⇦ Эволюция API
  62. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev type OrderType { items: [ItemType]!

    userId: ID! userName: String! } 62 type UserType { id: ID! name: String! } type OrderType { items: [ItemType]! user: UserType! } ✅ Не возвращайте данные других сущностей
  63. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev type UserType { id: ID!

    name: String! moderationStatus: ModerationStatusType! moderationDate: DateTime! } 63 Выделяйте связанные данные в типы type UserModerationType { status: ModerationStatusType! date: DateTime! } type UserType { id: ID! name: String! moderation: UserModerationType! } ✅
  64. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev type ProductType { id: ID!

    } type ItemType { product: ProductType! quantity: Int! } type OrderType { items: [ItemType]! } 64 Дайте доступ и к данным и к логике Как клиент может проверить наличие определенного товара в заказе?
  65. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 65 const query = gql`

    query GetOrder($id: ID!) { order(id: $id) { items { product { id } } } } ` const order = request(query, { id }) const found = order.items.find(item "=> item.product.id "== productId) if (found ""!== null) { "//… } Дайте доступ и к данным и к логике
  66. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 66 type ProductType { id:

    ID! } type ItemType { product: ProductType! quantity: Int! } type OrderType { items: [ItemType]! hasProduct(id: ID!): Bool! } Дайте доступ и к данным и к логике
  67. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 67 const query = gql`

    query GetOrder($id: ID!, $productId: ID!) { order(id: $id) { hasProduct(productId: $productId) } } ` const order = request(query, { id, productId }) if (order.hasProduct) { "//… } Дайте доступ и к данным и к логике
  68. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev Пример проектирования мутаций: блог •

    две страницы - страница чтения and форма редактирования • title и content могут быть изменены на форме редактирования • статья может быть опубликована/спрятана с помощью кнопки на странице чтения • тэги могут быть изменены с помощью выпадающего списка на странице чтения 68
  69. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev type MutationType { createPost( title:

    String, content: String, tags: [String], published: Bool ): PostType! updatePost( id: ID!, title: String, content: String, tags: [String], published: Bool ): PostType! deletePost(id: ID!): Bool } CRUD 69
  70. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 70 input PostInput { title:

    String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! } Атомарные мутации
  71. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 71 input PostInput { title:

    String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! } Атомарные мутации
  72. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 72 input PostInput { title:

    String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! } Input • CQRS - Command Query Responsibility Segregation • поля в input могут быть только scalar или input
  73. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 73 input PostInput { title:

    String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! } Атомарные мутации
  74. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 74 input PostInput { title:

    String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! } Атомарные мутации
  75. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 75 Язык запросов Схема API

    Реализация API Что может пойти не так Как проектировать схему ⇨ Эволюция API ⇦
  76. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 76 type ItemType { id:

    ID! quantity: Integer! } Аддитивные изменения безопасны type ItemType { id: ID! quantity: Integer! addedAt: DateTime! }
  77. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 77 type ItemType { quantity:

    Integer! count: Integer! @deprecated( reason: "Use `quantity`." ) } @deprecated
  78. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 78 Мониторинг запросов

  79. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 79 Аварийное отключение мутаций mutation

    { transferMoney(to: 42, amount: 100) { id } } { "data": null, "errors": [ { "mutationDeprecated": "transferMoney", "message": "Please update your app" } ] }
  80. !80 Зачем использовать GraphQL? • нет overfetching и underfetching •

    схема API - часть технологии: - документация - валидация запросов • предсказуемое поведение бэкенда • подписки
  81. IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 81 evl.ms/blog

  82. evl.ms/blog @dmitrytsepelev DmitryTsepelev @evilmartians evl.ms/telegram Спасибо за внимание! IT NIGHTS

    2019 82