Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

DUMP2019 DmitryTsepelev 2 evilmartians.com

Slide 3

Slide 3 text

DUMP2019 DmitryTsepelev 3 Что такое GraphQL? • язык запросов к API • среда исполнения запросов по схеме

Slide 4

Slide 4 text

DUMP2019 DmitryTsepelev 4 Agenda • язык запросов • описание схемы • среда исполнения запросов • реализация бэкенда • как проектировать схему • средства документации

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

DUMP2019 DmitryTsepelev 12 Представление данных в GraphQL – граф

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

DUMP2019 DmitryTsepelev 14 Запрос: query { me { name } } Ответ: { "data": { "me" { "name": "John Doe" } } } GraphQL – язык запросов к API

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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 } } } одинаковые наборы полей фрагмент

Slide 20

Slide 20 text

DUMP2019 DmitryTsepelev Транспорт • согласно спецификации – transport-agnostic • в реализациях: - HTTP POST - один эндпоинт (например /graphql) - код ответа 200 - OK 20

Slide 21

Slide 21 text

DUMP2019 DmitryTsepelev 21 query { user(id: 404) { id name } } { "data": null, "errors": [{ "message": "User not found" }] } Представление ошибок

Slide 22

Slide 22 text

DUMP2019 DmitryTsepelev • нужные данные одним запросом 22 Почему GraphQL?

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 } }

Slide 25

Slide 25 text

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/

Slide 26

Slide 26 text

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/

Slide 27

Slide 27 text

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/

Slide 28

Slide 28 text

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/

Slide 29

Slide 29 text

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" } } ] }

Slide 30

Slide 30 text

DUMP2019 DmitryTsepelev • дублирование данных в ответе 30 GraphQL - где подвох?

Slide 31

Slide 31 text

DUMP2019 DmitryTsepelev 31 query { articles { id author { name } } } { "data": { "articles" [ { "id": "1", "author": { "name": "John" } }, { "id": "2", "author": { "name": "John" } }, ] } } Дублирование данных в GraphQL

Slide 32

Slide 32 text

DUMP2019 DmitryTsepelev GraphQL - где подвох? • дублирование данных в ответе • нельзя запросить дерево произвольной глубины 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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 …

Slide 37

Slide 37 text

DUMP2019 DmitryTsepelev 37 query { user(id: 42) { orders { user { orders { user { … } } } } } } Сложность и глубина запросов

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

DUMP2019 DmitryTsepelev • нужные данные одним запросом • механизм получения обновлений 41 Почему GraphQL?

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

DUMP2019 DmitryTsepelev 45 Схема - описание типов и связей

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

DUMP2019 DmitryTsepelev 48 type UserType { name: String! orders(page: Int): [OrderType]! } Один из встроенных скалярных типов Схема: скалярные типы

Slide 49

Slide 49 text

DUMP2019 DmitryTsepelev • Int • Float • String • Boolean • ID 49 Схема: встроенные скалярные типы

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

DUMP2019 DmitryTsepelev 51 type UserType { name: String! orders(page: Int): [OrderType]! } Поля могут иметь аргументы Схема: аргументы

Slide 52

Slide 52 text

DUMP2019 DmitryTsepelev 52 type QueryType { users: [UserType]! } type MutationType { signUp(login: String!, password: String!): UserType! } type SubscriptionType { userSignedUp: UserType! } Схема: корневые типы

Slide 53

Slide 53 text

DUMP2019 DmitryTsepelev 53 GraphQL – среда исполнения запросов

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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"] }] }

Slide 56

Slide 56 text

DUMP2019 DmitryTsepelev 56 type UserType { name: String! moderationStatus: String! } Схема: enum

Slide 57

Slide 57 text

DUMP2019 DmitryTsepelev 57 ALLOWED_STATUSES = ["moderation", "approved", "rejected"] if ALLOWED_STATUSES.include(status) { "//… } else { "//… } Схема: enum в мутациях придется валидировать значение вручную

Slide 58

Slide 58 text

DUMP2019 DmitryTsepelev 58 enum UserModerationStatus { MODERATION APPROVED REJECTED } type UserType { name: String! moderationStatus: UserModerationStatus! } Схема: enum

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

DUMP2019 DmitryTsepelev 62 Как писать бэкенд

Slide 63

Slide 63 text

DUMP2019 DmitryTsepelev 63 class QueryType < BaseType field :users, [UserType], null: false def users User.all # SELECT * FROM users; end end QueryType на бэке

Slide 64

Slide 64 text

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 Реализация типов на бэке

Slide 65

Slide 65 text

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 Реализация типов на бэке

Slide 66

Slide 66 text

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 Реализация типов на бэке

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

DUMP2019 DmitryTsepelev 68 Проектируем схему правильно

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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! } ✅

Slide 71

Slide 71 text

DUMP2019 DmitryTsepelev type ProductType { id: ID! } type ItemType { product: ProductType! quantity: Int! } type OrderType { items: [ItemType]! } Как клиент может проверить наличие определенного товара в заказе? 71 Хорошая схема: логика внутри типов

Slide 72

Slide 72 text

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) { "//… } Хорошая схема: логика внутри типов

Slide 73

Slide 73 text

DUMP2019 DmitryTsepelev 73 type ProductType { id: ID! } type ItemType { product: [ProductType]! quantity: Int! } type OrderType { items: [ItemType]! hasProduct(id: ID!): Bool! } Хорошая схема: логика внутри типов

Slide 74

Slide 74 text

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) { "//… } Хорошая схема: логика внутри типов

Slide 75

Slide 75 text

DUMP2019 DmitryTsepelev Хорошая схема: мутации для атомарных действий Задача: • приложение для редактирования и просмотра статей • страница просмотра публикации и форма редактирования • на форме редактирования - название и контент • publish/unpublish по кнопке на странице поста • добавление тэгов через dropdown на странице поста 75

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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! } Хорошая схема: мутации для атомарных действий

Slide 78

Slide 78 text

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! } Хорошая схема: мутации для атомарных действий

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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! } Хорошая схема: мутации для атомарных действий

Slide 81

Slide 81 text

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! } Хорошая схема: мутации для атомарных действий

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

DUMP2019 DmitryTsepelev 83 GraphiQL https://github.com/graphql/graphiql

Slide 84

Slide 84 text

DUMP2019 DmitryTsepelev 84 GraphiQL class FilmType < BaseType field :title, String, "The title of this film" end

Slide 85

Slide 85 text

DUMP2019 DmitryTsepelev 85 https://github.com/2fd/graphdoc GraphDoc

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

DUMP2019 DmitryTsepelev Спасибо за внимание! 90 evl.ms/blog @dmitrytsepelev DmitryTsepelev @evilmartians_ru evl.ms/telegram