Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

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

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

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

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

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

Dmitry Tsepelev

April 19, 2019
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

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

    представление структуры БД Дмитрий Цепелев, Evil Martians
  2. DUMP2019 DmitryTsepelev 3 Что такое GraphQL? • язык запросов к

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

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

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

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

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

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

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

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

    Как реализовать действие присоединения пользователя к сообществу? 11 зачем клиенту знать про эту сущность?
  11. DUMP2019 DmitryTsepelev 14 Запрос: query { me { name }

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

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

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

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

    orders { items { product { title } quantity } } } } variables { "userId": 42 } GraphQL – язык запросов к API
  16. 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 } } } одинаковые наборы полей фрагмент
  17. DUMP2019 DmitryTsepelev Транспорт • согласно спецификации – transport-agnostic • в

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

    } } { "data": null, "errors": [{ "message": "User not found" }] } Представление ошибок
  19. 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": "[email protected]", "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 } }
  20. 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/
  21. 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/
  22. 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/
  23. 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/
  24. 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" } } ] }
  25. DUMP2019 DmitryTsepelev 31 query { articles { id author {

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

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

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

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

    ответе • нельзя запросить дерево произвольной глубины • сложно кэшировать запросы • N+1 35
  30. 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 …
  31. DUMP2019 DmitryTsepelev 37 query { user(id: 42) { orders {

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

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

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

    content" ) { id } } { "data": { "createPost" { "id": 4 } } } Мутации: механизм изменения данных
  35. DUMP2019 DmitryTsepelev 42 subscription { postCreated { id title content

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

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

    обновлений • расширяемая система типов 44 Почему GraphQL?
  38. DUMP2019 DmitryTsepelev 46 type UserType { name: String! orders(page: Int:

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

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

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

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

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

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

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

    обновлений • расширяемая система типов • валидация запросов 54 Почему GraphQL?
  46. 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"] }] }
  47. DUMP2019 DmitryTsepelev 57 ALLOWED_STATUSES = ["moderation", "approved", "rejected"] if ALLOWED_STATUSES.include(status)

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

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

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

    placedAt: DateTime! } { id: 12, placedAt: “2019-03-01T19:18:37+03:00" } Схема: scalar
  51. 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
  52. DUMP2019 DmitryTsepelev 63 class QueryType < BaseType field :users, [UserType],

    null: false def users User.all # SELECT * FROM users; end end QueryType на бэке
  53. 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 Реализация типов на бэке
  54. 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 Реализация типов на бэке
  55. 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 Реализация типов на бэке
  56. 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
  57. DUMP2019 DmitryTsepelev type OrderType { items: [ItemType]! userId: ID! userName:

    String! } 69 Хорошая схема: только поля сущности в типе type UserType { id: ID! name: String! } type OrderType { items: [ItemType]! user: UserType! } ✅
  58. 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! } ✅
  59. DUMP2019 DmitryTsepelev type ProductType { id: ID! } type ItemType

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

    ItemType { product: [ProductType]! quantity: Int! } type OrderType { items: [ItemType]! hasProduct(id: ID!): Bool! } Хорошая схема: логика внутри типов
  62. 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) { "//… } Хорошая схема: логика внутри типов
  63. DUMP2019 DmitryTsepelev Хорошая схема: мутации для атомарных действий Задача: •

    приложение для редактирования и просмотра статей • страница просмотра публикации и форма редактирования • на форме редактирования - название и контент • publish/unpublish по кнопке на странице поста • добавление тэгов через dropdown на странице поста 75
  64. 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
  65. 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! } Хорошая схема: мутации для атомарных действий
  66. 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! } Хорошая схема: мутации для атомарных действий
  67. 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
  68. 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! } Хорошая схема: мутации для атомарных действий
  69. 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! } Хорошая схема: мутации для атомарных действий
  70. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

    обновлений • расширяемая система типов • валидация запросов • средства документации 82 Почему GraphQL?
  71. DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения

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

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

    дерево произвольной глубины • сложно кэшировать запросы • N+1 • overkill для очень простых API 89 GraphQL - где подвох?