Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQLとHaskell

 GraphQLとHaskell

Haskell Day 2021発表資料

B60e45c2f2adbfef9aa2d7c200b27f67?s=128

Daishi Nakajima

November 07, 2021
Tweet

Transcript

  1. GraphQLとHaskell Haskell Day 2021 中嶋大嗣

  2. 自己紹介 • 中嶋大嗣 • Twitter: @nakaji_dayo • 趣味Haskeller • BEエンジニア@カンム

    • Haskell Day 2018 • Haskell/Servantで行う安全かつ高速なAPI開発 • https://speakerdeck.com/daishi/servantdexing-uan-quan-katugao- su-naapikai-fa
  3. 前提知識 • Haskell、Webの基礎知識 • GraphQLの知識は不要

  4. アジェンダ • GraphQLの基礎 • HaskellでのGraphQL APIの実装 • N+1問題の解決 サンプル実装: https://github.com/nakaji-dayo/hello-graphql

  5. GraphQLとは 基礎的な部分について説明します

  6. GraphQLとは • GraphQLはAPIの為のクエリ言語、及び型システムを使いクエ リを実行するサーバーサイドランタイム • RESTと比較されることが多い • Facebookが2015年に公開

  7. • 複雑なFEからの要求に答える • 1ページの表示のために何度もAPIをたたく • 各ページからの要求により、レスポンスが巨大化or クエリの複雑化 GET /users?active=1 なぜGraphQLが必要

    Page1 GET /posts GET /messages Page1 Page2 GET /users?page=1 GET /users?page=1 写真も欲しい 友人の一覧が欲しい 友人については名前が欲しい RESTと複雑化する FEからの要求
  8. なぜGraphQLが必要 • 仕様・ドキュメントの管理が大変・実装とのずれ • 仕様記述のための言語が定義されている(スキーマ定義) • サーバー側・クライアント側共にコード生成などで型検査 • ドキュメント生成や開発補助ツールが存在 •

    ※因みにRESTでも • Swagger生成など • Haskell/Servantで行う安全かつ高速なAPI開発等々 • Client sideのエコシステムの充実、Subscription等
  9. 言語仕様紹介 • スキーマ定義言語とクエリ言語 type Query { me: User } type

    User { id: ID name: String } { me { name } } API仕様定義 クライアント APIサーバー Schemaをもとに有効なクエリを作成 型などでSchemaと定義を共有
  10. スキーマ定義言語の仕様 • Scalar • Int, Float, String, Boolean, ID, 独自定義

    • Enum enum Style { LAGER ALE STOUT }
  11. スキーマ定義言語の仕様 • List, not-null • Object, Field [Style] String! type

    Store { name: String! address: String beers(style: Style = ALE): [Beer!]! } Default Nullable Fieldに引数を定義可能
  12. スキーマ定義言語の仕様 • 他、UnionやInterfaceが用意されている • 省略🙇 union SearchResult = Beer |

    Store | User interface Drink { id: ID! name: String! abv: Float }
  13. スキーマ定義言語の仕様 • Query, Mutation, Subscription Type schema { query: Query

    mutation: Mutation subscription: Subscription } type Query { stores(name: String): [Store!]! beer(id: ID!): Beer } エントリポイント定義の為の特別な型 Queryのエントリポイント
  14. クエリ言語の仕様 query StoreDetail { stores { name } } このqueryに任意の名前を付ける

    Query型から要求するfieldを選択 Store型から要求するfieldを選択
  15. クエリ言語の仕様 query StoreDetail { stores(name: ”hoge”) { name beers {

    name } } } beersも要求 引数を追加
  16. クエリ言語の仕様 query StoreDetail { stores(name: ”hoge”) { name beers {

    name } } todayBeer: beer(id: 123) { name } } Queryの複数fieldに一括問い合わせ Fieldにエイリアスを設定可能
  17. 実装してみる HaskellでGraphQL APIを実装していきます

  18. HaskellのGraphQLライブラリ • 開発が現時点でActiveなもの Morpheus-graphql Mu-haskell Kind Schema First / Code

    First Schema First Query ✔ ✔ Mutation ✔ ✔ Subscription ✔ ✔ Client ✔ gqlのFieldのHaskellで の表現 レコード構文 型レベルリスト
  19. 実装 • Morpheus-graphql • Code first • 流れ 1. Query型を定義

    2. 型に沿い実装 3. 実行してみる
  20. Query型定義 data Store m = Store { id :: StoreId

    , name :: Text , beers :: m [Beer] } deriving (Generic, GQLType) data Beer = Beer { id :: BeerId , name :: Text , ibu :: Maybe Int } deriving (Generic, GQLType) ストア ビール N N ストアのビール一覧 RDBでの想定
  21. Query型定義 data Query m = Query { store :: StoreArgs

    -> m (Store m) , stores :: StoresArgs -> m [Store m] , beer :: BeerArgs -> m Beer , bestBeer :: m Beer } deriving (Generic, GQLType) data StoresArgs = StoresArgs { name :: Maybe Text } deriving (Generic, GQLType) data Style = Lager | Ale | Stout deriving (Generic, GQLType) クエリの実行環境 Resolver o event m value Resolver: フィールドの要求に応じて データ取得や計算を行う → enum
  22. GraphQL Schemaの生成 toGraphQLDocument (Proxy @ (RootResolver IO () Query Undefined

    Undefined)) … type Store { id: StoreId! name: String! beers: [Beer!]! } … type Query { store(id: StoreId!): Store! … } schema { query: Query } これをクライアントや他ツールに提供 型から生成 Mutation, Subscription
  23. Resolverの実装 queryR :: Query (Resolver QUERY e AppM) queryR =

    Query { store = storeR , stores = storesR , beer = beerR , bestBeer = bestBeerR } RootResolver AppM () Query Undefined Undefined) StoreArgs -> m (Store m) DB接続情報など → IO
  24. Resolverの実装 storeR :: StoreArgs -> ResolverQ e AppM Store storeR

    StoreArgs { id } = lift $ renderStore <$> getStore id renderStore :: E.Store -> Store (Resolver QUERY e AppM) renderStore x = Store { id = x ^. #id , name = x ^. #name & pack , beers = beersR (x ^. #id) } データ取得(DB問い合わせ) 返却する型に詰める 別のResolverを指定 resolverA resolverB resolverC query { A { B } }
  25. Resolverの実装 • 動作確認のためにログ仕込んだ getStore :: E.StoreId -> AppM E.Store getStore

    id = do debug ("getStore", id) head <$> queryM selectStore id getBeers :: E.StoreId -> AppM [E.Beer] getBeers id = do debug ("getBeers", id) queryM selectBeersByStoreId id StoreId
  26. 動作確認 • 実行してみる • 任意のWebFW内で使用可能 • (今回はscottyに乗せた) gqlApi :: ByteString

    -> IO ByteString gqlApi = interpreter rootResolver
  27. 動作確認 • 単一ストア取得 query { store(id: "c62eddb5-56e2- 4b91-9a84-02c4412f62c8") { id

    name } } { "data": { "store": { "id": "c62eddb5-56e2-4b91-9a84- 02c4412f62c8", "name": "storeX" } } } getStore "c62eddb5-56e2-4b91-9a84-02c4412f62c8"
  28. 動作確認 • ビールも取得 query { store(id: "c62eddb5-56e2- 4b91-9a84-02c4412f62c8") { id

    name beers { name } } } { "data": { "store": { "id": "c62eddb5-56e2-4b91-9a84- 02c4412f62c8", "beers": [ { "name": "fuga:0" }, { "name": "fuga:1" } ], "name": "storeX" } } } getStore "c62eddb5-56e2-4b91-9a84-02c4412f62c8" getBeers "c62eddb5-56e2-4b91-9a84-02c4412f62c8" Beersも取得 クエリに応じて必要なデータ問い合わせのみ行われている!
  29. N+1問題の解決 Haxlを使いN+1問題の解決を試みます

  30. N+1って • 問題となるクエリ例 • 複数ストアを所持するbeersと共に取得 query { stores { id

    beers { name } } }
  31. N+1って • 現状の実装 storesR :: StoresArgs -> ComposedResolver QUERY e

    AppM [] Store storesR _ = lift $ fmap renderStore <$> getStores renderStore x = Store { … , beers = beersR (x ^. #id) } beersR :: StoreId -> ResolverQ e AppM [Beer] beersR id = lift $ fmap renderBeer <$> getBeers id ストアN個取得 各ストアに対して beersR :: StoreId -> ResolverQ e AppM [Beer] beersR id = lift $ fmap renderBeer <$> getBeers id ビール一覧を取得
  32. N+1って • 実行結果 getStores getBeers "c62eddb5-56e2-4b91-9a84-02c4412f62c8" getBeers "97412f4a-456c-4965-afc3-5ea8dccf02bc" getBeers "3a215480-ca66-4571-a609-b21bccae42ec"

    getStores getBeers(store=A) A getBeers(store=B) B getBeers(store=C) C 件数増えたら… さらにネストされたら…
  33. Haxlとは • DBやWebAPIなど複数のリモートデータへのアクセスを単純化 するライブラリ • 主な機能 • Applicativeによる暗黙の並列化 • データフェッチの自動バッチ化

    • リクエスト・計算のキャッシュ • Facebook製 個別に見ていきます
  34. Haxlの機能> 並列化 参考資料: [1] Simon Marlow Facebook (FHPC ‘17, September

    2017). Haskell in the datacentre! https://simonmar.github.io/slides/Haskell%20in%20the%20datacentre.pdf numCommonFriends a b = do fa <- friendsOf a fb <- friendsOf b return (length (intersect fa fb)) friendsOf a friendsOf b intersect fa fb friendsOf a friendsOf b intersect fa fb 引用元 [1]
  35. Haxlの機能>並列化 numCommonFriends a b = do fa <- friendsOf a

    fb <- friendsOf b return (length (intersect fa fb)) friendsOf a >>= (\x1-> friendsOf b >>= (\x2 -> return length (intersect fa fb)) )) numCommonFriends a b = (length . intersect) <$> friendsOf a <*> friendsOf b
  36. Haxlの機能>並列化 (>>=) :: Monad m => m a -> (a

    -> m b) -> m b (<*>) :: Applicative f => f (a -> b) -> f a -> f b 独立
  37. Haxlの機能>並列化 numCommonFriends:: User -> User => Haxl Int numCommonFriends a

    b = (length . intersect) <$> friendsOf a <*> friendsOf b friendsOf a friendsOf b intersect fa fb 人間が考えて書くのか?
  38. Haxlの機能 > 並列化 • GHC ApplicativeDo拡張 • 依存を見て<$>,<*>を使ったdo構文の脱糖を行う do x1

    <- A x2 <- B x3 <- C x1 x4 <- D x2 return (x1,x2,x3,x4) join ((\x1 x2 -> (\x3 x4 - > (x1,x2,x3,x4)) <$> C x1 <*> D x2)) <$> A <*> B) B (x1, x2, x3, x4) A D x2 C x1 暗黙の並列化
  39. Haxlの機能>バッチ化 • Haxlの使い方から追う 1. Request型を定義 2. DataSourceのインスタンス化(fetchを実装) data MyRequest a

    where GetStore :: StoreId -> MyRequest Store GetStores :: MyRequest [Store] GetBeers :: StoreId -> MyRequest [Beer] GetBeer :: BeerId -> MyRequest Beer レスポンスの型
  40. • Haxlの使い方から追う 1. Request型を定義 2. DataSourceのインスタンス化(= fetchを実装) Haxlの機能>バッチ化 Fetch ::

    DataSource u req => State req -> Flags -> u -> PerformFetch req data PerformFetch req = SyncFetch ([BlockedFetch req] -> IO ()) | AsyncFetch ([BlockedFetch req] -> IO () -> IO ()) | BackgroundFetch ([BlockedFetch req] -> IO ()) [(リクエスト, 結果の書き込み先)] -> IO ()
  41. • Haxlの使い方から追う 1. Request型を定義 2. DataSourceのインスタンス化(= fetchを実装) Haxlの機能>バッチ化 [(リクエスト, 結果の書き込み先)]

    -> IO () 並列実行可能なリクエスト [GetStore 1, GetStore 2, GetBeer 5] Select * from store where id in (1,2) Select * from beer where id in (5)
  42. Haxlの機能>キャッシュ • HaxlはRequestにHashableを要求 instance Hashable (MyRequest a) where hashWithSalt s

    (GetStore (StoreId id)) = hashWithSalt s (0 :: Int, id)
  43. Haxlを導入 1. Haxl DataSourceを実装 ➔ 省略…🙇 2. 最終的に次のHaxlアクションができた getStore ::

    StoreId -> Haxl Store getStore id = dataFetch (GetStore id) getBeers :: StoreId -> Haxl [Beer] getBeers id = dataFetch (GetBeersByStore id) 本発表のサンプルコードやHaxl公式ドキュメントをご参照ください Solving the "N+1 Selects Problem" with Haxl, https://github.com/facebook/Haxl/blob/main/example/sql/readme. md
  44. Haxlを導入 storesR :: StoresArgs -> ComposedResolver QUERY e AppM []

    Store storesR _ = lift $ fmap renderStore <$> getStores beersR :: StoreId -> ResolverQ e AppM [Beer] beersR id = lift $ fmap renderBeer <$> getBeers id 元のコード storesR :: StoresArgs -> ComposedResolver QUERY e Haxl [] Store storesR _ = lift $ fmap renderStore <$> getStores beersR :: StoreId -> ResolverQ e Haxl [Beer] beersR id = lift $ fmap renderBeer <$> getBeers id Haxlアクションに差し替え 変更箇所 AppM → Haxl
  45. N+1問題の解決 fetch stores fetchBeers [ "c62eddb5-56e2-4b91-9a84- 02c4412f62c8“, "97412f4a-456c-4965-afc3- 5ea8dccf02bc", "3a215480-ca66-4571-a609-

    b21bccae42ec“ ] query { stores { id beers { name } } } N+1が解決された!しかも ・プログラムの構造に影響を与えていない ・静的
  46. ついでに解決、キャッシュ query { beer(id:"b1aa264a-7062-4dce-a3c0- 1353ae98f151") { name } bestBeer {

    id name } } 仮に同じBeerを示す場合、 キャッシュが使われる beerR id = fetchBeeer id bestBeerR = do topId:_ <- getRanking fetchBeer topId Cache HIT ① ②
  47. まとめ • GraphQLの基本 • HaskellでのMorpheus-graphqlを使った実装 • Haxlを使いプログラムの構造を保ちつつN+1の解決