Slide 1

Slide 1 text

GraphQLとHaskell Haskell Day 2021 中嶋大嗣

Slide 2

Slide 2 text

自己紹介 • 中嶋大嗣 • Twitter: @nakaji_dayo • 趣味Haskeller • BEエンジニア@カンム • Haskell Day 2018 • Haskell/Servantで行う安全かつ高速なAPI開発 • https://speakerdeck.com/daishi/servantdexing-uan-quan-katugao- su-naapikai-fa

Slide 3

Slide 3 text

前提知識 • Haskell、Webの基礎知識 • GraphQLの知識は不要

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

• 複雑な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からの要求

Slide 8

Slide 8 text

なぜGraphQLが必要 • 仕様・ドキュメントの管理が大変・実装とのずれ • 仕様記述のための言語が定義されている(スキーマ定義) • サーバー側・クライアント側共にコード生成などで型検査 • ドキュメント生成や開発補助ツールが存在 • ※因みにRESTでも • Swagger生成など • Haskell/Servantで行う安全かつ高速なAPI開発等々 • Client sideのエコシステムの充実、Subscription等

Slide 9

Slide 9 text

言語仕様紹介 • スキーマ定義言語とクエリ言語 type Query { me: User } type User { id: ID name: String } { me { name } } API仕様定義 クライアント APIサーバー Schemaをもとに有効なクエリを作成 型などでSchemaと定義を共有

Slide 10

Slide 10 text

スキーマ定義言語の仕様 • Scalar • Int, Float, String, Boolean, ID, 独自定義 • Enum enum Style { LAGER ALE STOUT }

Slide 11

Slide 11 text

スキーマ定義言語の仕様 • List, not-null • Object, Field [Style] String! type Store { name: String! address: String beers(style: Style = ALE): [Beer!]! } Default Nullable Fieldに引数を定義可能

Slide 12

Slide 12 text

スキーマ定義言語の仕様 • 他、UnionやInterfaceが用意されている • 省略🙇 union SearchResult = Beer | Store | User interface Drink { id: ID! name: String! abv: Float }

Slide 13

Slide 13 text

スキーマ定義言語の仕様 • Query, Mutation, Subscription Type schema { query: Query mutation: Mutation subscription: Subscription } type Query { stores(name: String): [Store!]! beer(id: ID!): Beer } エントリポイント定義の為の特別な型 Queryのエントリポイント

Slide 14

Slide 14 text

クエリ言語の仕様 query StoreDetail { stores { name } } このqueryに任意の名前を付ける Query型から要求するfieldを選択 Store型から要求するfieldを選択

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

クエリ言語の仕様 query StoreDetail { stores(name: ”hoge”) { name beers { name } } todayBeer: beer(id: 123) { name } } Queryの複数fieldに一括問い合わせ Fieldにエイリアスを設定可能

Slide 17

Slide 17 text

実装してみる HaskellでGraphQL APIを実装していきます

Slide 18

Slide 18 text

HaskellのGraphQLライブラリ • 開発が現時点でActiveなもの Morpheus-graphql Mu-haskell Kind Schema First / Code First Schema First Query ✔ ✔ Mutation ✔ ✔ Subscription ✔ ✔ Client ✔ gqlのFieldのHaskellで の表現 レコード構文 型レベルリスト

Slide 19

Slide 19 text

実装 • Morpheus-graphql • Code first • 流れ 1. Query型を定義 2. 型に沿い実装 3. 実行してみる

Slide 20

Slide 20 text

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での想定

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 } }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

動作確認 • 実行してみる • 任意のWebFW内で使用可能 • (今回はscottyに乗せた) gqlApi :: ByteString -> IO ByteString gqlApi = interpreter rootResolver

Slide 27

Slide 27 text

動作確認 • 単一ストア取得 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"

Slide 28

Slide 28 text

動作確認 • ビールも取得 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も取得 クエリに応じて必要なデータ問い合わせのみ行われている!

Slide 29

Slide 29 text

N+1問題の解決 Haxlを使いN+1問題の解決を試みます

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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 ビール一覧を取得

Slide 32

Slide 32 text

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 件数増えたら… さらにネストされたら…

Slide 33

Slide 33 text

Haxlとは • DBやWebAPIなど複数のリモートデータへのアクセスを単純化 するライブラリ • 主な機能 • Applicativeによる暗黙の並列化 • データフェッチの自動バッチ化 • リクエスト・計算のキャッシュ • Facebook製 個別に見ていきます

Slide 34

Slide 34 text

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]

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Haxlの機能>並列化 (>>=) :: Monad m => m a -> (a -> m b) -> m b (<*>) :: Applicative f => f (a -> b) -> f a -> f b 独立

Slide 37

Slide 37 text

Haxlの機能>並列化 numCommonFriends:: User -> User => Haxl Int numCommonFriends a b = (length . intersect) <$> friendsOf a <*> friendsOf b friendsOf a friendsOf b intersect fa fb 人間が考えて書くのか?

Slide 38

Slide 38 text

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 暗黙の並列化

Slide 39

Slide 39 text

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 レスポンスの型

Slide 40

Slide 40 text

• 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 ()

Slide 41

Slide 41 text

• 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)

Slide 42

Slide 42 text

Haxlの機能>キャッシュ • HaxlはRequestにHashableを要求 instance Hashable (MyRequest a) where hashWithSalt s (GetStore (StoreId id)) = hashWithSalt s (0 :: Int, id)

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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が解決された!しかも ・プログラムの構造に影響を与えていない ・静的

Slide 46

Slide 46 text

ついでに解決、キャッシュ 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 ① ②

Slide 47

Slide 47 text

まとめ • GraphQLの基本 • HaskellでのMorpheus-graphqlを使った実装 • Haxlを使いプログラムの構造を保ちつつN+1の解決