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

GraphQLとHaskell

 GraphQLとHaskell

Haskell Day 2021発表資料

Daishi Nakajima

November 07, 2021
Tweet

More Decks by Daishi Nakajima

Other Decks in Programming

Transcript

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

    • Haskell Day 2018 • Haskell/Servantで行う安全かつ高速なAPI開発 • https://speakerdeck.com/daishi/servantdexing-uan-quan-katugao- su-naapikai-fa
  2. • 複雑な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からの要求
  3. 言語仕様紹介 • スキーマ定義言語とクエリ言語 type Query { me: User } type

    User { id: ID name: String } { me { name } } API仕様定義 クライアント APIサーバー Schemaをもとに有効なクエリを作成 型などでSchemaと定義を共有
  4. スキーマ定義言語の仕様 • List, not-null • Object, Field [Style] String! type

    Store { name: String! address: String beers(style: Style = ALE): [Beer!]! } Default Nullable Fieldに引数を定義可能
  5. スキーマ定義言語の仕様 • Query, Mutation, Subscription Type schema { query: Query

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

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

    name } } todayBeer: beer(id: 123) { name } } Queryの複数fieldに一括問い合わせ Fieldにエイリアスを設定可能
  8. HaskellのGraphQLライブラリ • 開発が現時点でActiveなもの Morpheus-graphql Mu-haskell Kind Schema First / Code

    First Schema First Query ✔ ✔ Mutation ✔ ✔ Subscription ✔ ✔ Client ✔ gqlのFieldのHaskellで の表現 レコード構文 型レベルリスト
  9. 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での想定
  10. 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
  11. 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
  12. 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
  13. 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 } }
  14. 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
  15. 動作確認 • 単一ストア取得 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"
  16. 動作確認 • ビールも取得 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も取得 クエリに応じて必要なデータ問い合わせのみ行われている!
  17. 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 ビール一覧を取得
  18. 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 件数増えたら… さらにネストされたら…
  19. 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]
  20. 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
  21. Haxlの機能>並列化 (>>=) :: Monad m => m a -> (a

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

    b = (length . intersect) <$> friendsOf a <*> friendsOf b friendsOf a friendsOf b intersect fa fb 人間が考えて書くのか?
  23. 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 暗黙の並列化
  24. 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 レスポンスの型
  25. • 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 ()
  26. • 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)
  27. 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
  28. 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
  29. 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が解決された!しかも ・プログラムの構造に影響を与えていない ・静的
  30. ついでに解決、キャッシュ 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 ① ②