Slide 1

Slide 1 text

1 カスタムしながら理解する GraphQL Connection yana-gi Kaigi on Rails 2024 2024.10.25

Slide 2

Slide 2 text

2 ⾃⼰紹介 minne事業部 プロダクト開発チーム 2022年 中途⼊社 yana-gi やなぎ ● 2021年 フィヨルドブートキャンプ卒業 ● 2022年〜 GMOペパボ株式会社 Webアプリケーションエンジニア ● GitHub : yana-gi ● X : @yana_gis ● ビールとお茶とみはしのあんみつが好き

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

4 ● webの主な技術スタック ○ Ruby on Rails ○ Next.js ○ MySQL ● 段階的に移⾏中 ○ フロント画⾯をRailsからNext.jsへ ○ APIをREST APIからGraphQL APIへ ● GraphQL APIの実装はgraphql-ruby minne

Slide 5

Slide 5 text

5 ● WebはNext.jsに移⾏済み ● GraphQL APIに移⾏済み この画⾯のAPIの実装の話👉 minneの検索画⾯

Slide 6

Slide 6 text

6 質問 🙋

Slide 7

Slide 7 text

7 GraphQLでAPIを 実装したことがある⽅ 🙋

Slide 8

Slide 8 text

8 GraphQLについて 初めて聞いた or 馴染みがない⽅ 🙋

Slide 9

Slide 9 text

9 1. GraphQLとは 1.1. minneでのGraphQL API使⽤例 1.2. minneで新規検索エンジンを導⼊する 1.3. Connection Type 2. Custom Connection 2.1. クエリをしてからデータを返すまで 2.2. Custom Connectionの実装 2.3. 遅延評価 アジェンダ

Slide 10

Slide 10 text

GraphQLとは 10 クライアントが必要なデータだけを指定して取得できる データクエリ⾔語及びランタイム ™

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

Products 
 Category 
 Viewer 
 Cart


Slide 13

Slide 13 text

GraphQLとは 13 エンドポイントで取得するデータを指定する ● GET /api/products.json ● GET /api/categories.json ● GET /api/cart.json ● GET /api/viewer.json 1ページを表⽰するのに4回APIを実⾏する必要がある REST APIの場合

Slide 14

Slide 14 text

14 ● 複数のデータ(ノード)を ⼀つのリクエストで取得する GraphQL APIの場合 GraphQLとは query productPage{ products { ... } categories { ... } cart { ... } viewer { ... } }

Slide 15

Slide 15 text

15 不要なデータのattributesまで取得してしまう Request : GET /api/products.json Response :👉 REST APIの場合 GraphQLとは

Slide 16

Slide 16 text

16 必要なデータ(ノード)の フィールドを指定できる GraphQL APIの場合 GraphQLとは query productPage { products { id name price } categories { id slug name } cart { ... } viewer { ... } }

Slide 17

Slide 17 text

17 ● 既存の検索エンジン(Elasticsearch) ● 新しい検索エンジン 新しい検索エンジンの導⼊ 🆕

Slide 18

Slide 18 text

18 ● クライアントが利⽤するminne APIはGraphQL API ○ クエリは既存の検索エンジンと同じように取得できるように ○ 既存のクエリのページネーションは カーソルに加えてオフセットでも取得できる ● 新検索エンジンのAPIはREST API ○ ページネーションはカーソルページネーションのみ 検索エンジン導⼊の要件

Slide 19

Slide 19 text

19 検索エンジン導⼊の要件

Slide 20

Slide 20 text

20 ● minne側で実装するAPIはGraphQL API ○ クエリは既存の検索エンジンと同じように取得できるように ○ 既存のクエリのページネーションは カーソルに加えてオフセットでも取得できる ● 新検索エンジンのAPIはREST API ○ ページネーションはカーソルページネーションのみ 検索エンジン導⼊の要件

Slide 21

Slide 21 text

21 ● オフセットページネーション ● カーソルページネーション ページネーションの種類

Slide 22

Slide 22 text

● データの開始位置を指定する ● offset と limit で取得する ○ offset : 何件⽬から取得するか ○ limit : 何件取得するか SQLの場合 オフセットページネーション 22 SELECT * FROM products LIMIT 10 OFFSET 30;

Slide 23

Slide 23 text

● 特定のデータポイント(カーソル)を基準に次のデー タを取得する ● 時系列などの特定のデータをキーとして検索する ○ cursor : ソート可能なデータポイント(カーソル) ○ limit : 何件取得するか SQLの場合 カーソルページネーション 23 SELECT * FROM products WHERE id > 30 LIMIT 10;

Slide 24

Slide 24 text

改めて検索エンジンの導⼊要件を確認 24 ◯ オフセット
 ◯ カーソル
 ◯ オフセット
 ✗ カーソル


Slide 25

Slide 25 text

GraphQLでページネーションを簡単に実装できる GraphQL Connection 25

Slide 26

Slide 26 text

● Connection: ノード(データ)を リスト形式で取得する ● Edge: ノード間の関係性や付加情報 ● Node: 実際のデータ本体 Connection のノードの取得クエリ 26 query { products(first: 10) { # ProductConnection Type edges { # ProductEdge Type cursor node { # Product Type name id }, …

Slide 27

Slide 27 text

nodesからでも取得できる 27 Connection のノードの取得クエリ query { products(first: 10) { nodes { name id } } }

Slide 28

Slide 28 text

開始位置を指定する 28 Connection のノードの取得クエリ # abc以降のデータを10件取得する team { members(first: 10, after: "abc") { nodes { id name } } }

Slide 29

Slide 29 text

29 Connection Typeの実装 field :items, Types::ItemType.connection_type, null: false def products object.products # ActiveRecord Relationの場合 end fieldのTypeをconnection_typeに指定する サポートされているクラスの場合、要素を全て渡す

Slide 30

Slide 30 text

● 検索エンジンはREST APIで提供されている ○ ページネーションはoffsetページネーションのみ ● 結果を取得したデータを扱うクラスは ActiveRecordではない ○ Connection Typeでサポートされていないクラスになりそう ○ サポートされていない場合は?🤔 改めて今回やりたいこと 30

Slide 31

Slide 31 text

Custom Connection 31

Slide 32

Slide 32 text

Custom Connectionを実装する Custom Connection Typeの実装 32 https://graphql-ruby.org/pagination/using_connections.html#return-collections def products # ActiveRecordのリレーションを取得 relation = object.items # 作成したCustom Connection Typeのインスタンスを返す Connections::ProductsConnection.new(relation) end 参考: https://graphql-ruby.org/pagination/using_connections.html#return-collections

Slide 33

Slide 33 text

GraphQL::Pagination::Connection を継承したConnection Typeを作る 実装する必要があるメソッドが4つある Custom Connectionの実装 33 参考: https://graphql-ruby.org/pagination/custom_connections.html class Connections::ProductsConnection < GraphQL::Pagination::Connection def nodes; ...; end # 返すべきノードの配列を取得 def has_next_page; ...; end # 次のページが存在するかを判定 def has_previous_page; ...; end # 前のページが存在するかを判定 def cursor_for(item); ...; end # 指定されたノードのカーソルを⽣成 end

Slide 34

Slide 34 text

● offset(開始位置)はどこで渡している? ○ 検索エンジンAPIリクエスト時にoffsetを指定できない ⾃分が疑問に思ったこと 🤔 34

Slide 35

Slide 35 text

真相を知るために、クエリを実⾏してから値が返却される までを追う ● シンプルなActive Recordの場合 ● Active RecordかつConnectionの場合 クエリを実⾏してから値が返却されるまで 35

Slide 36

Slide 36 text

クエリを実⾏してから値が返却されるまで 単純にProductクラスを返す場合 36

Slide 37

Slide 37 text

クエリを実⾏してから値が返却されるまで Active RecordでConnectionを返す場合 1 37

Slide 38

Slide 38 text

クエリを実⾏してから値が返却されるまで Active RecordでConnectionを返す場合 2 38

Slide 39

Slide 39 text

クエリを実⾏してから値が返却されるまで Active RecordでConnectionを返す場合 3 39

Slide 40

Slide 40 text

クエリを実⾏してから値が返却されるまで Active RecordでConnectionを返す場合 4 40 offsetやlimitの計算 


Slide 41

Slide 41 text

1. 疑問に思ったことの答え ● Connection の offset (開始位置)はどうやって決まる? → Connection Class でcursorから計算している 2. resolverを通った後にConnection Class で offset が決まる わかったこと💡 41

Slide 42

Slide 42 text

● resolverの時点ではoffsetとlimitが決まっていない ● resolverの段階でAPI リクエストをするにもoffsetと limitが決まっていない さらなる疑問🤔 42

Slide 43

Slide 43 text

● Promiseクラスで遅延評価を⾏う ○ ※ 既存の検索エンジンの実装を流⽤ 答え 43

Slide 44

Slide 44 text

遅延評価の流れ1 44

Slide 45

Slide 45 text

45 遅延評価の流れ2

Slide 46

Slide 46 text

46 遅延評価の流れ3

Slide 47

Slide 47 text

クエリをしてから値が返却されるまで1 47

Slide 48

Slide 48 text

クエリをしてから値が返却されるまで2 48

Slide 49

Slide 49 text

クエリをしてから値が返却されるまで3 49

Slide 50

Slide 50 text

クエリをしてから値が返却されるまで4 50

Slide 51

Slide 51 text

1. GraphQLでページングを⾏うにはConnectionを使う 2. 独⾃のモデルでConnectionを使うにはCustom Connectionを定義する 3. Connection Type Classでoffsetが決まる 4. offsetを利⽤するためにresolverではPromiseクラスを利 ⽤して遅延評価を⾏う まとめ & 実装してみて分かったこと 51

Slide 52

Slide 52 text

● 1回のクエリで検索エンジン APIに複数回リクエストがされ ている 1. total_countやhas_next_pageなどを計算するタイミング 2. productsの値を取得するタイミング ● total_countやhas_next_pageのみを取得するエンドポイン トを作ってもらうなどの対応が必要そう 課題 52

Slide 53

Slide 53 text

● チームメンバー ○ kazu(@kazuhitonakayam)さん ○ saki(@Saki-htr)さん ● 資料のレビュー ○ daiki(@doew)さん ○ pyamaさん ● ⼤元のPromiseクラスやCustom Connection Typeの実装 ○ ogidowさん special thanks 53

Slide 54

Slide 54 text

54 Thank you! よいGraphQLライフを!