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

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

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

Доклад предназначен для тех, кто пока не разрабатывал свои API на GraphQL, а также для тех, кто попробовал и не увидел особой разницы с REST.

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

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

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

April 19, 2019
Tweet

Transcript

  1. Как мыслить графами или почему GraphQL – это не просто

    представление структуры БД Дмитрий Цепелев, Evil Martians
  2. DUMP2019 DmitryTsepelev 2 evilmartians.com

  3. DUMP2019 DmitryTsepelev 3 Что такое GraphQL? • язык запросов к

    API • среда исполнения запросов по схеме
  4. DUMP2019 DmitryTsepelev 4 Agenda • язык запросов • описание схемы

    • среда исполнения запросов • реализация бэкенда • как проектировать схему • средства документации
  5. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы GET users

    GET users/:id POST users PATCH users/:id DELETE users/:id • данные – ресурсы, идентифицируются с помощью URL • действия – HTTP verbs • связи через ключи 5
  6. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы GET users

    GET users/:id POST users PATCH users/:id DELETE users/:id • данные – ресурсы, идентифицируются с помощью URL • действия – HTTP verbs • связи через ключи 6
  7. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы GET users/42

    { "id": 42, "name": "John Doe", "accountId": 23 } • данные – ресурсы, идентифицируются с помощью URL • действия – HTTP verbs • связи через ключи 7
  8. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы POST users/42/join-community

    POST communities/67/join 8 Как реализовать действие присоединения пользователя к сообществу?
  9. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы POST users/42/join-community

    POST communities/67/join 9 действие идентифицируется через URL и HTTP verb Как реализовать действие присоединения пользователя к сообществу?
  10. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы POST community-memberships

    Как реализовать действие присоединения пользователя к сообществу? 10
  11. DUMP2019 DmitryTsepelev Представление данных в REST – ресурсы POST community-memberships

    Как реализовать действие присоединения пользователя к сообществу? 11 зачем клиенту знать про эту сущность?
  12. DUMP2019 DmitryTsepelev 12 Представление данных в GraphQL – граф

  13. DUMP2019 DmitryTsepelev 13 GraphQL – язык запросов к API

  14. DUMP2019 DmitryTsepelev 14 Запрос: query { me { name }

    } Ответ: { "data": { "me" { "name": "John Doe" } } } GraphQL – язык запросов к API
  15. DUMP2019 DmitryTsepelev 15 Запрос: query { user(id: 42) { orders

    { items { product { title } quantity } } } } Ответ: { "data": { "user" { "orders": [ { "items": [ { "product": { title: "iPhone 7" }, "quantity": 2 } ] } ] } } } GraphQL – язык запросов к API
  16. DUMP2019 DmitryTsepelev 16 Запрос: query { user(id: 42) { orders

    { items { product { title } quantity } } } } Ответ: { "data": { "user" { "orders": [ { "items": [ { "product": { title: "iPhone 7" }, "quantity": 2 } ] } ] } } } GraphQL – язык запросов к API
  17. DUMP2019 DmitryTsepelev 17 query FetchUser($userId: Int) { user(id: $userId) {

    orders { items { product { title } quantity } } } } variables { "userId": 42 } GraphQL – язык запросов к API
  18. DUMP2019 DmitryTsepelev 18 query FetchUser($userId: Int) { user(id: $userId) {

    orders { items { product { title } quantity } } } } variables { "userId": 42 } GraphQL – язык запросов к API
  19. DUMP2019 DmitryTsepelev 19 query { me { orders { items

    { product { title } quantity } } cart { items { product { title } quantity } } } } GraphQL – язык запросов к API fragment orderFields on OrderType { items { product { title } quantity } } query { me { orders { …orderFields } cart { …orderFields } } } одинаковые наборы полей фрагмент
  20. DUMP2019 DmitryTsepelev Транспорт • согласно спецификации – transport-agnostic • в

    реализациях: - HTTP POST - один эндпоинт (например /graphql) - код ответа 200 - OK 20
  21. DUMP2019 DmitryTsepelev 21 query { user(id: 404) { id name

    } } { "data": null, "errors": [{ "message": "User not found" }] } Представление ошибок
  22. DUMP2019 DmitryTsepelev • нужные данные одним запросом 22 Почему GraphQL?

  23. DUMP2019 DmitryTsepelev 23 GET /user GET /repos/:owner/:repo GET /repos/:owner/:repo/issues Underfetching

    в REST
  24. DUMP2019 DmitryTsepelev 24 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 } }
  25. DUMP2019 DmitryTsepelev 25 query { user(login: "DmitryTsepelev") { name }

    repository(owner: "graphql", name: "graphiql") { watchers { totalCount } forks { totalCount } stargazers { totalCount } issues { totalCount } pullRequests { totalCount } issues { edges { node { title createdAt author { login } } } } } } GitHub GraphQL API https://developer.github.com/v4/explorer/
  26. DUMP2019 DmitryTsepelev 26 query { user(login: "DmitryTsepelev") { name }

    repository(owner: "graphql", name: "graphiql") { watchers { totalCount } forks { totalCount } stargazers { totalCount } issues { totalCount } pullRequests { totalCount } issues { edges { node { title createdAt author { login } } } } } } GitHub GraphQL API https://developer.github.com/v4/explorer/
  27. DUMP2019 DmitryTsepelev 27 query { user(login: "DmitryTsepelev") { name }

    repository(owner: "graphql", name: "graphiql") { watchers { totalCount } forks { totalCount } stargazers { totalCount } issues { totalCount } pullRequests { totalCount } issues { edges { node { title createdAt author { login } } } } } } GitHub GraphQL API https://developer.github.com/v4/explorer/
  28. DUMP2019 DmitryTsepelev 28 query { user(login: "DmitryTsepelev") { name }

    repository(owner: "graphql", name: "graphiql") { watchers { totalCount } forks { totalCount } stargazers { totalCount } issues { totalCount } pullRequests { totalCount } issues { edges { node { title createdAt author { login } } } } } } GitHub GraphQL API https://developer.github.com/v4/explorer/
  29. DUMP2019 DmitryTsepelev Стандарт JSON:API (https://jsonapi.org) 29 GET /articles?include=author&fields[articles]=title,body&fields[author]=name { "data":

    [{ "type": "articles", "id": "1", "attributes": { "title": "JSON:API paints my bikeshed!", "body": "The shortest article. Ever." }, "relationships": { "author": { "data": {"id": "42", "type": "people"} } } }], "included": [ { "type": "people", "id": "42", "attributes": { "name": "John" } } ] }
  30. DUMP2019 DmitryTsepelev • дублирование данных в ответе 30 GraphQL -

    где подвох?
  31. DUMP2019 DmitryTsepelev 31 query { articles { id author {

    name } } } { "data": { "articles" [ { "id": "1", "author": { "name": "John" } }, { "id": "2", "author": { "name": "John" } }, ] } } Дублирование данных в GraphQL
  32. DUMP2019 DmitryTsepelev GraphQL - где подвох? • дублирование данных в

    ответе • нельзя запросить дерево произвольной глубины 32
  33. DUMP2019 DmitryTsepelev 33 type CategoryType { name: String! subcategories: [CategoryType]!

    } query { categories { name subcategories { name subcategories { name } } } } Нельзя запросить дерево произвольной глубины
  34. DUMP2019 DmitryTsepelev GraphQL - где подвох? • дублирование данных в

    ответе • нельзя запросить дерево произвольной глубины • сложно кэшировать запросы 34
  35. DUMP2019 DmitryTsepelev GraphQL - где подвох? • дублирование данных в

    ответе • нельзя запросить дерево произвольной глубины • сложно кэшировать запросы • N+1 35
  36. DUMP2019 DmitryTsepelev 36 Проблема N + 1 SELECT id, title

    FROM issues WHERE issues.repository_id = 32126 SELECT id, name FROM users WHERE user.id = 314 SELECT id, name FROM users WHERE user.id = 2513 SELECT id, name FROM users WHERE user.id = 55231 …
  37. DUMP2019 DmitryTsepelev 37 query { user(id: 42) { orders {

    user { orders { user { … } } } } } } Сложность и глубина запросов
  38. DUMP2019 DmitryTsepelev 38 mutation { createPost( title: "draft", content: "no

    content" ) { id } } { "data": { "createPost" { "id": 4 } } } Мутации: механизм изменения данных
  39. DUMP2019 DmitryTsepelev 39 mutation { createPost( title: "draft", content: "no

    content" ) { id } } { "data": { "createPost" { "id": 4 } } } Мутации: механизм изменения данных
  40. DUMP2019 DmitryTsepelev 40 mutation { createPost( title: "draft", content: "no

    content" ) { id } } { "data": { "createPost" { "id": 4 } } } Мутации: механизм изменения данных
  41. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

    обновлений 41 Почему GraphQL?
  42. DUMP2019 DmitryTsepelev 42 subscription { postCreated { id title content

    } } { "data": { "postCreated" { "id": 4, "title": "draft", "content": "no content" } } } Подписки: механизм получения обновлений
  43. DUMP2019 DmitryTsepelev 43 subscription { postCreated { id title content

    } } { "data": { "postCreated" { "id": 4, "title": "draft", "content": "no content" } } } Подписки: механизм получения обновлений
  44. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

    обновлений • расширяемая система типов 44 Почему GraphQL?
  45. DUMP2019 DmitryTsepelev 45 Схема - описание типов и связей

  46. DUMP2019 DmitryTsepelev 46 type UserType { name: String! orders(page: Int:

    [OrderType]! } Object Type – тип с набором полей Схема: типы
  47. DUMP2019 DmitryTsepelev 47 type UserType { name: String! orders(page: Int):

    [OrderType]! } Поля, принадлежащие типу Схема: поля типов
  48. DUMP2019 DmitryTsepelev 48 type UserType { name: String! orders(page: Int):

    [OrderType]! } Один из встроенных скалярных типов Схема: скалярные типы
  49. DUMP2019 DmitryTsepelev • Int • Float • String • Boolean

    • ID 49 Схема: встроенные скалярные типы
  50. DUMP2019 DmitryTsepelev 50 type UserType { name: String! orders(page: Int):

    [OrderType]! } Представление списка объектов Схема: списки
  51. DUMP2019 DmitryTsepelev 51 type UserType { name: String! orders(page: Int):

    [OrderType]! } Поля могут иметь аргументы Схема: аргументы
  52. DUMP2019 DmitryTsepelev 52 type QueryType { users: [UserType]! } type

    MutationType { signUp(login: String!, password: String!): UserType! } type SubscriptionType { userSignedUp: UserType! } Схема: корневые типы
  53. DUMP2019 DmitryTsepelev 53 GraphQL – среда исполнения запросов

  54. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

    обновлений • расширяемая система типов • валидация запросов 54 Почему GraphQL?
  55. DUMP2019 DmitryTsepelev 55 Схема: валидация запросов 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"] }] }
  56. DUMP2019 DmitryTsepelev 56 type UserType { name: String! moderationStatus: String!

    } Схема: enum
  57. DUMP2019 DmitryTsepelev 57 ALLOWED_STATUSES = ["moderation", "approved", "rejected"] if ALLOWED_STATUSES.include(status)

    { "//… } else { "//… } Схема: enum в мутациях придется валидировать значение вручную
  58. DUMP2019 DmitryTsepelev 58 enum UserModerationStatus { MODERATION APPROVED REJECTED }

    type UserType { name: String! moderationStatus: UserModerationStatus! } Схема: enum
  59. DUMP2019 DmitryTsepelev 59 type OrderType { id: ID! placedAt: String!

    } { id: 12, placedAt: “2019-03-01T19:18:37+03:00" } Схема: scalar
  60. DUMP2019 DmitryTsepelev 60 scalar DateTime type OrderType { id: ID!

    placedAt: DateTime! } { id: 12, placedAt: “2019-03-01T19:18:37+03:00" } Схема: scalar
  61. DUMP2019 DmitryTsepelev 61 class DateTimeType < BaseScalar def self.coerce_input(value, _context)

    Time.zone.parse(value) end def self.coerce_result(value, _context) value.utc.iso8601 end end Схема: scalar
  62. DUMP2019 DmitryTsepelev 62 Как писать бэкенд

  63. DUMP2019 DmitryTsepelev 63 class QueryType < BaseType field :users, [UserType],

    null: false def users User.all # SELECT * FROM users; end end QueryType на бэке
  64. DUMP2019 DmitryTsepelev 64 class UserType < BaseType 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 Реализация типов на бэке
  65. DUMP2019 DmitryTsepelev 65 class UserType < BaseType 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 Реализация типов на бэке
  66. DUMP2019 DmitryTsepelev 66 class UserType < BaseType 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 Реализация типов на бэке
  67. DUMP2019 DmitryTsepelev 67 class UserType < BaseType field :orders, [OrderType],

    null: false end class QueryType < BaseType field :users, [UserType], null: false def users # SELECT * FROM users; # SELECT * FROM orders where user_id IN (…); User.preload(:orders) end end Решаем проблему с N+1
  68. DUMP2019 DmitryTsepelev 68 Проектируем схему правильно

  69. DUMP2019 DmitryTsepelev type OrderType { items: [ItemType]! userId: ID! userName:

    String! } 69 Хорошая схема: только поля сущности в типе type UserType { id: ID! name: String! } type OrderType { items: [ItemType]! user: UserType! } ✅
  70. DUMP2019 DmitryTsepelev type UserType { id: ID! name: String! moderationStatus:

    ModerationStatusType! moderationDate: DateTime! } 70 Хорошая схема: только поля сущности в типе type UserModerationType { status: ModerationStatusType! date: DateTime! } type UserType { id: ID! name: String! moderation: UserModerationType! } ✅
  71. DUMP2019 DmitryTsepelev type ProductType { id: ID! } type ItemType

    { product: ProductType! quantity: Int! } type OrderType { items: [ItemType]! } Как клиент может проверить наличие определенного товара в заказе? 71 Хорошая схема: логика внутри типов
  72. DUMP2019 DmitryTsepelev 72 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) { "//… } Хорошая схема: логика внутри типов
  73. DUMP2019 DmitryTsepelev 73 type ProductType { id: ID! } type

    ItemType { product: [ProductType]! quantity: Int! } type OrderType { items: [ItemType]! hasProduct(id: ID!): Bool! } Хорошая схема: логика внутри типов
  74. DUMP2019 DmitryTsepelev 74 const query = gql` query GetOrder($id: ID!,

    $productId: ID!) { order(id: $id) { hasProduct(productId: $productId) } } ` const order = request(query, { id, productId }) if (order.hasProduct) { "//… } Хорошая схема: логика внутри типов
  75. DUMP2019 DmitryTsepelev Хорошая схема: мутации для атомарных действий Задача: •

    приложение для редактирования и просмотра статей • страница просмотра публикации и форма редактирования • на форме редактирования - название и контент • publish/unpublish по кнопке на странице поста • добавление тэгов через dropdown на странице поста 75
  76. DUMP2019 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 76
  77. DUMP2019 DmitryTsepelev 77 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! } Хорошая схема: мутации для атомарных действий
  78. DUMP2019 DmitryTsepelev 78 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! } Хорошая схема: мутации для атомарных действий
  79. DUMP2019 DmitryTsepelev 79 input PostInput { title: String, content: String

    } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! } Inputs • CQRS - Command Query Responsibility Segregation • внутри input - только scalar и input
  80. DUMP2019 DmitryTsepelev 80 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! } Хорошая схема: мутации для атомарных действий
  81. DUMP2019 DmitryTsepelev 81 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! } Хорошая схема: мутации для атомарных действий
  82. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

    обновлений • расширяемая система типов • валидация запросов • средства документации 82 Почему GraphQL?
  83. DUMP2019 DmitryTsepelev 83 GraphiQL https://github.com/graphql/graphiql

  84. DUMP2019 DmitryTsepelev 84 GraphiQL class FilmType < BaseType field :title,

    String, "The title of this film" end
  85. DUMP2019 DmitryTsepelev 85 https://github.com/2fd/graphdoc GraphDoc

  86. DUMP2019 DmitryTsepelev 86 https://github.com/APIs-guru/graphql-voyager GraphQL Voyager

  87. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

    обновлений • расширяемая система типов • валидация запросов • средства документации • механизм эволюции API 87 Почему GraphQL?
  88. DUMP2019 DmitryTsepelev • мониторинг запросов • механизм эволюции – директива

    @deprecated • аварийное отключение мутаций 88 type ItemType { id: ID! quantity: Integer! count: Integer! @deprecated(reason: "Use `quantity`.") } Эволюция API
  89. DUMP2019 DmitryTsepelev • дублирование данных в ответе • нельзя запросить

    дерево произвольной глубины • сложно кэшировать запросы • N+1 • overkill для очень простых API 89 GraphQL - где подвох?
  90. DUMP2019 DmitryTsepelev Спасибо за внимание! 90 evl.ms/blog @dmitrytsepelev DmitryTsepelev @evilmartians_ru

    evl.ms/telegram