Slide 1

Slide 1 text

КАК МЫСЛИТЬ ГРАФАМИ или Почему GraphQL - это не просто представление структуры БД Дмитрий Цепелев Злые Марсиане

Slide 2

Slide 2 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 2

Slide 3

Slide 3 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 3 evilmartians.com

Slide 4

Slide 4 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 4 evilmartians.com

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev POST /users/42/join-community POST /communities/67/join 9 действие определено через URL + HTTP verb Как правильно?

Slide 10

Slide 10 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 10 В REST-стиле POST /community-memberships

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 15 GraphQL позволяет получить все данные одним запросом

Slide 16

Slide 16 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 16 GET /user GET /repos/:owner/:repo GET /repos/:owner/:repo/issues Underfetching в REST

Slide 17

Slide 17 text

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": "[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 18

Slide 18 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev Что такое GraphQL? • язык запросов • среда исполнения 18

Slide 19

Slide 19 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 19 ⇨ Язык запросов ⇦ Схема API Реализация API Что может пойти не так Как проектировать схему Эволюция API

Slide 20

Slide 20 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 20 Request: query { currentUserId } Response: { "data": { "currentUserId": 42 } } Поля

Slide 21

Slide 21 text

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 } ] } ] } } } Аргументы

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev Транспортный уровень 25 • согласно спецификации – transport-agnostic • в реализациях: - HTTP POST - один эндпоинт - код ответа 200 - OK

Slide 26

Slide 26 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 26 Request: query { user(id: 404) { id name } } Response: { "data": null, "errors": [ { "message": "User not found" } ] } Представление ошибок

Slide 27

Slide 27 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 27 Мутации: обновление данных

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 29 Request: subscription { postCreated { id title content } } Response: { "data": { "postCreated": { "id": 4, "title": "draft", "content": "no content" } } } Подписки: получение обновлений

Slide 30

Slide 30 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 30 Язык запросов ⇨ Схема API ⇦ Реализация API Что может пойти не так Как проектировать схему Эволюция API

Slide 31

Slide 31 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 31 type UserType { name: String! orders(page: Int): [OrderType]! } Типы

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 36 type QueryType { users: [UserType]! } type MutationType { signUp(login: String!, password: String!): UserType! } type SubscriptionType { userSignedUp: UserType! } Корневые типы

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 38 enum UserModerationStatus { MODERATION APPROVED REJECTED } type UserType { name: String! moderationStatus: UserModerationStatus! } Перечисления

Slide 39

Slide 39 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 39 scalar DateTime type OrderType { placedAt: DateTime! } { "placedAt": "2019-03-01T19:18:37+03:00" } Пользовательские скалярные типы

Slide 40

Slide 40 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 40 Автоматическая документация: GraphiQL github.com/graphql/graphiql

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 42 Язык запросов Схема API ⇨ Реализация API ⇦ Что может пойти не так Как проектировать схему Эволюция API

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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 Объявление типа на бэке: объявляем поле

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 50 Язык запросов Схема API Реализация API ⇨ Что может пойти не так ⇦ Как проектировать схему Эволюция API

Slide 51

Slide 51 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 51 query { user(id: 42) { orders { user { orders { user { … } } } } } } Сложность и глубина запросов

Slide 52

Slide 52 text

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" } }, ] } } Дублирование данных в ответе

Slide 53

Slide 53 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 53 type CategoryType { name: String! subcategories: [CategoryType]! } query { categories { name subcategories { name subcategories { name } } } } Нет рекурсивных запросов

Slide 54

Slide 54 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 54 Сложно кэшировать ответы • REST: ответ зависит от HTTP verb и URL • GraphQL: ответ зависит от selection set • нет легкого и эффективного способа кэшировать ответ GraphQL $

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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: работаем с запросом

Slide 59

Slide 59 text

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: работаем с запросом

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 61 Язык запросов Схема API Реализация API Что может пойти не так ⇨ Как проектировать схему ⇦ Эволюция API

Slide 62

Slide 62 text

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! } ✅ Не возвращайте данные других сущностей

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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) { "//… } Дайте доступ и к данным и к логике

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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) { "//… } Дайте доступ и к данным и к логике

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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! } Атомарные мутации

Slide 71

Slide 71 text

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! } Атомарные мутации

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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! } Атомарные мутации

Slide 74

Slide 74 text

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! } Атомарные мутации

Slide 75

Slide 75 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 75 Язык запросов Схема API Реализация API Что может пойти не так Как проектировать схему ⇨ Эволюция API ⇦

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 77 type ItemType { quantity: Integer! count: Integer! @deprecated( reason: "Use `quantity`." ) } @deprecated

Slide 78

Slide 78 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 78 Мониторинг запросов

Slide 79

Slide 79 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 79 Аварийное отключение мутаций mutation { transferMoney(to: 42, amount: 100) { id } } { "data": null, "errors": [ { "mutationDeprecated": "transferMoney", "message": "Please update your app" } ] }

Slide 80

Slide 80 text

!80 Зачем использовать GraphQL? • нет overfetching и underfetching • схема API - часть технологии: - документация - валидация запросов • предсказуемое поведение бэкенда • подписки

Slide 81

Slide 81 text

IT NIGHTS 2019 DmitryTsepelev @dmitrytsepelev 81 evl.ms/blog

Slide 82

Slide 82 text

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