Slide 1

Slide 1 text

本番環境のRailsプロダクトで GraphQL APIを実装した話 2019/7/4 Ryosuke Yamamoto

Slide 2

Slide 2 text

2 自己紹介 Ryosuke Yamamoto ( @roolrool ) Webエンジニア 男子シンクロのインストラクター、 Webディレクターを経て2017年1 月にスペースマーケットにジョイン。 開発比率:バックエンド 5:5 フロントエンド

Slide 3

Slide 3 text

3 担当施策で直近の大きなリリース

Slide 4

Slide 4 text

4 スペースマーケットEVENTにチケット決済機能追加!

Slide 5

Slide 5 text

5 決済リリース前 参照系: GraphQL API / 更新系: REST API 決済リリース後 すべてGraphQL APIに移行 スペースマーケットEVENTのAPI構成

Slide 6

Slide 6 text

6 弊社のGraphQL APIの実装で 工夫しているポイントについて話します! 参考: RailsでGraphQL APIを作る時に悩んだ5つのこと

Slide 7

Slide 7 text

7 GraphQL

Slide 8

Slide 8 text

8 ● クエリ言語とスキーマ言語からなるAPIのための規格 ○ クエリ ■ リクエスト用の言語 ■ 更新系: Mutation ■ 参照系: Query ○ スキーマ ■ データ型を定義 ● 単一エンドポイント ○ /graphql GraphQLとは

Slide 9

Slide 9 text

9 スキーマ クエリ レスポンス GraphQLの例 クエリをresolverで処理してレスポンスを返す

Slide 10

Slide 10 text

10 詳しくはgfxさんの記事をどうぞ

Slide 11

Slide 11 text

11 ※前提 APIのフレームワークがRailsなので、 graphql-rubyを使用してGraphQL APIを実装

Slide 12

Slide 12 text

12 話すこと ● ページネーション ● N+1問題 ● 画像/ファイルアップロード ● アクセス権限管理

Slide 13

Slide 13 text

13 ページネーション

Slide 14

Slide 14 text

14 ● 標準のものはRelayのCursor Connectionというカーソルベースのページ ネーション ● graphql-rubyもRelayの仕様をサポート ○ 導入しやすい GraphQLのページネーション

Slide 15

Slide 15 text

15 カーソル・・?? (初めて知った瞬間の脳内 )

Slide 16

Slide 16 text

16 ● レコードごとにユニークなID(カーソル)を割り当て、そのカーソルを起点として 前後のデータを取得する方式 ● レコードの増減によるずれが起きない点がメリット カーソルベースのページネーション

Slide 17

Slide 17 text

17 カーソルベースのページネーション node (データそのもの) cursor (edgeごとのユニークなID) page(edges+pageInfo) edge ・・・・

Slide 18

Slide 18 text

18 件数 起点

Slide 19

Slide 19 text

19 ● アプリケーションの仕様上、Offset&Limitページネーションが必要だった ● カーソルベースはTwitterタイムラインのような無限スクロールコンテンツには向 いているがページ数を指定したい場合に向かない カーソルベースの問題点

Slide 20

Slide 20 text

20 ということで Offset&Limitページネーションを実装

Slide 21

Slide 21 text

21 ● Railsのページネーションのデファクトと言えばkaminari ○ ページネーション機能と関連メソッドを提供するgem ■ 例) object.page(1).per(10).total_pages: 合計ページ数 Offset&Limitページネーションの実装

Slide 22

Slide 22 text

22 kaminariを利用して独自のpageInfoを定義

Slide 23

Slide 23 text

23 カーソルベース ページネーション比較 Offset&Limit

Slide 24

Slide 24 text

24 N+1問題

Slide 25

Slide 25 text

25 N+1問題 GraphQLは何も対策しなければサクッとN+1が発生する →resolverを再帰的に実行するのでその度にSQLが  発行されてしまうため

Slide 26

Slide 26 text

26 サクッとリクエスト N+1問題

Slide 27

Slide 27 text

27 サクッとN+1 N+1問題

Slide 28

Slide 28 text

28 N+1?eager loadingだ!(^q^)

Slide 29

Slide 29 text

29 ● GraphQLに先読み(eager loading)は不向き ○ 回避自体は可能 ○ どのテーブルがひかれるかどうかはクエリ次第なので先読みしても使わ れない可能性がある ○ associationのfieldを追加する度に先読みを追加することになる eager loadingで回避? SQLを遅延評価したい

Slide 30

Slide 30 text

30 ● resolverの実行の度にSQLを解決せず、必要なデータを蓄積してから 最後にまとめて解決する ● クエリに対して必要なSQLだけを発行するので無駄がない ● javascriptならdataloader ● Rubyならgraphql-batch SQLの遅延評価

Slide 31

Slide 31 text

31 ● resolverの実行の度にSQLを解決せず、必要なデータを蓄積してから 最後にまとめて解決する ● クエリに対して必要なSQLだけを発行するので無駄がない ● javascriptならdataloader ● Rubyならgraphql-batch SQLの遅延評価

Slide 32

Slide 32 text

32 graphql-batch

Slide 33

Slide 33 text

33 ● Shopify製のライブラリ ● 内部で非同期処理のgem(lgierth/promise.rb)を使用している ● 単体ではActiveRecordに依存していないが、サンプルが用意されているので その通りに実装すればOK ○ AssociationLoader ○ RecordLoader graphql-batchとは

Slide 34

Slide 34 text

34 AssociationLoader

Slide 35

Slide 35 text

35 ● has_manyな関連レコードを取得する際に使用 ● オブジェクトの関連レコードをすべて返す AssociationLoaderとは

Slide 36

Slide 36 text

36 confidential AssociationLoader定義

Slide 37

Slide 37 text

37 AssociationLoader呼び出し user.events のイメージ

Slide 38

Slide 38 text

38 ● 下記理由で使用せず ○ 弊社では独自にページネーションを実装していたため関連をまるまる取 得する形では要件を満たせなかった ○ resolver内で絞り込みを行った結果を返したかった AssociationLoaderは不採用

Slide 39

Slide 39 text

39 RecordLoader

Slide 40

Slide 40 text

40 ● belongs_to or has_manyな関連レコードを取得する際に使用 ● 指定したモデル名&渡したidのレコードを返す RecordLoaderとは

Slide 41

Slide 41 text

41 RecordLoader定義

Slide 42

Slide 42 text

42 RecordLoader呼び出し

Slide 43

Slide 43 text

43 ● idを単体か配列で渡せば遅延評価してくれる ○ 絞り込んだ結果のレコードセットのidを渡せばいいので柔軟 RecordLoaderを採用

Slide 44

Slide 44 text

44 ● has_many: ○ RecordLoader#load_many ● belongs_to: ○ RecordLoader#load ● has_one: × ※関連の外部キーを持たない場合 ○ 良い方法を調査中 各関連の対応状況

Slide 45

Slide 45 text

45 画像/ファイルアップロード

Slide 46

Slide 46 text

46 ● GraphQLサーバーへのデータの受け渡しはJSONが一般的 ● JSONではバイナリデータを扱うことができない ○ ファイルのアップロードには工夫が必要 GraphQL×ファイルアップロード

Slide 47

Slide 47 text

47 ● multipart/form-dataで送信 ○ htmlのform submitやjsのFormDataオブジェクトを使用 ● Base64にエンコードしてJSONで送信 アップロード方法の選択 Base64にエンコードする方法を採用

Slide 48

Slide 48 text

48 Base64に対応したアップローダー

Slide 49

Slide 49 text

49 バックエンドの実装

Slide 50

Slide 50 text

50 フロントエンドの実装

Slide 51

Slide 51 text

51 ● データサイズが大きくなる ○ エンコード前と比較して33%増加 ● オンメモリで扱うには巨大すぎるデータになってしまう可能性 ○ リクエストボディのサイズ制限は合わせて必要 Base64の問題点

Slide 52

Slide 52 text

52 アクセス権限管理 confidential

Slide 53

Slide 53 text

53 ● fieldやObjectTypeそのものに対してアクセス制限を入れたい ● ログインユーザーの個人情報や売上データ等 ○ 誰でも引けると困るデータ ○ 権限チェックをresolver毎に書くのはつらい アクセス権限管理の目的

Slide 54

Slide 54 text

54 一括で権限を設定できるライブラリを利用

Slide 55

Slide 55 text

55 アクセス権限の定義

Slide 56

Slide 56 text

56 schema定義時に宣言

Slide 57

Slide 57 text

57 所感など confidential

Slide 58

Slide 58 text

58 移行してみて ● good ○ ページに対して必要なデータだけを取得するという柔軟なリクエストが出 来るのはやはり良い ○ 加工したデータを返すfieldなどRESTではレスポンスサイズを気にして持 たせづらかったものも気軽に追加できるようになった ● bad ○ パフォーマンス分析のツールが揃っていない ■ Apollo ServerならApollo Engine ■ New Relic ● クライアント側でquery名をユニークにする必要あり

Slide 59

Slide 59 text

No content