KVS から複数件まとめて取得してデータ構築する
はじめに
KVS に限った話ではなく DB などでも同じようなことがありますが データ取得件数が多いときに一件づつ取得していると遅くなってしまう場合があります。
こういうときのために複数件まとめて取得する API が用意されている場合が多いと思います。 Redis には mget という API がありました。
この複数件取得する API とデータ構築などの処理を組み合わせるにはどうするのがいいかなと検討したという内容です。
https://github.com/bigsleep/redis-experiment1
今回使ったコードはここにあります。
やり方
要件としては下の二点になると思います。
- 取得に使うキーを集める
- KVS から取得したデータを変換して結果の型に変換する関数を合成する
最初は何かのモナドを使っていい感じに書けないかなと考えていました。
do name <- kvsGet "name0" id age <- kvsGet "age0" convertInt point <- kvsGet "point0" convertInt return (UserInfo name age point)
しかしモナドだと取得のキーにもモナド値が使われてしまう場合があり得ます。 例えば下のような感じで書かれてしまうとキーをまとめて取得するということができなくなります。
do name <- kvsGet "name0" id key <- kvsGet "key0" id age <- kvsGet key convertInt point <- kvsGet "point0" convertInt return (UserInfo name age point)
ということでこういうケースはモナドでなくアプリカティブを使うのがいいのではないかと考えました。
Haskell/Applicative functors - Wikibooks, open books for an open world
アプリカティブは >>=
関数が使えないのでモナド値がキーとして使われるのは制限されます。
アプリカティブのインスタンスを定義するあたりまでのコードは下のような感じになりました。
アプリカティブ則などをみたしているか不安なので Free Applicative などを使ってもよかったかもしれません。
type KvsConvert a = StateT [Maybe ByteString] Maybe a data KvsGets a = KvsGets [ByteString] (KvsConvert a) instance Functor KvsGets where fmap f (KvsGets keys convert) = KvsGets keys (fmap f convert) instance Applicative KvsGets where pure a = KvsGets [] (return a) KvsGets keys0 f <*> KvsGets keys1 a = KvsGets (keys0 ++ keys1) (f <*> a) kvsGet :: ByteString -> (Maybe ByteString -> Maybe a) -> KvsGets a kvsGet key convert = KvsGets [key] m where m = do vals <- State.get (h, tail) <- lift . uncons $ vals State.put tail lift . convert $ h hmgetAll :: KvsGets a -> Redis.Redis (Maybe a) hmgetAll (KvsGets keys m) = do vals <- Redis.mget keys either (const $ return Nothing) (return . State.evalStateT m) vals
Docker Compose で実行してみる
前の記事にも書きましたが Docker で haskell コンパイル環境を整えて使ってみています。
アプリケーション用のコンテナ以外に KVS やデータベースのコンテナを利用する場合には Docker Compose を使うと簡単にできるようだったので使ってみました。
Docker Compose | Docker Documentation
Docker Hub に Reids, Memcached, MySQL, PostgreSQL など Webアプリで使うものは大体揃っていて これらと連携するようなライブラリを書く時も便利そうでした。
https://hub.docker.com/_/redis/
https://hub.docker.com/_/memcached/
https://hub.docker.com/_/mysql
https://hub.docker.com/_/postgresql/
Docker Compose をインストールして docker-compose.yml を書いて docker-compose run
コマンドを実行するという感じで使えました。
docker-compose.yml は下のようになりました。
version: "2" services: redis: image: redis:4.0.9 ports: - "6379:6379" app: image: tkaaad97/haskell-docker:8.2.2 command: stack exec redis-experiment1-exe working_dir: /app volumes: - .:/app - .stack:/root/.stack depends_on: - redis
ビルド時のコマンドは
docker-compose run --rm app stack build
実行時のコマンドは
docker-compose run --rm app stack exec redis-experiment1-exe
コードの実行結果
実験用のコードは下の感じです。
data SomeData = SomeData Int String String [Int] deriving (Show) main :: IO () main = do let entries = [("a", "999"), ("b", "hello"), ("c", "world"), ("d", "[1,2,3]")] getList = sequenceA $ map (flip kvsGet id) ["a", "b", "c", "d"] :: KvsGets [ByteString] getSomeData = SomeData <$> kvsGet "a" (fmap $ read . UTF8.decode . BS.unpack) <*> kvsGet "b" (fmap $ UTF8.decode . BS.unpack) <*> kvsGet "c" (fmap $ UTF8.decode . BS.unpack) <*> kvsGet "d" (fmap $ read . UTF8.decode . BS.unpack) connectionInfo = Redis.defaultConnectInfo { Redis.connectHost = "redis" } connection <- Redis.checkedConnect connectionInfo Redis.runRedis connection $ do Redis.mset entries a <- hmgetAll getList liftIO . print $ a b <- hmgetAll getSomeData liftIO . print $ b c <- hmgetAll $ (,) <$> getList <*> getSomeData liftIO . print $ c
期待した通り結果が取得できました。
Just ["999","hello","world","[1,2,3]"] Just (SomeData 999 "hello" "world" [1,2,3]) Just (["999","hello","world","[1,2,3]"],SomeData 999 "hello" "world" [1,2,3])
まとめ
KVS から複数件まとめて取得してデータ構築する方法について検討してみました。 モナドは使えないけど、アプリカティブを使うことで合成しやすい感じで書けそうなことがわかりました。