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

GraphQLとHaskell

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

 GraphQLとHaskell

Haskell Day 2021発表資料

Avatar for Daishi Nakajima

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 ① ②