$30 off During Our Annual Pro Sale. View Details »

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

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

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

Dmitry Tsepelev

August 02, 2019
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

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