vironでNature Remoのダッシュボードを作った
こんにちは。
最近は、iPad Airが届いたので、どう使おうか色々模索しながら遊んでいます!
このブログもiPad Airで書いてみてます!
今日はネタ記事です。
僕の部屋では、Nature Remoを使ってエアコンの操作やお掃除ロボの定時起動などをしています。
Natrure Remoは、デバイス自身が温度と湿度と照度のセンサーを備えていて、公開されているREST API 経由でその値を取得できるので、それを使って今回は遊んでみました。
やったこと
今回は、vironというswagger定義とサーバーサイドのコードを書けば、デザインは自動で作ってくれるマネージメントコンソールアプリのOSSを使って、Nature Remoのデバイス管理と1日の温度などをグラフ化してみました。
github.com
使った技術
vironは、いくつかのフロントへ返すレスポンスさえ規則に従っておけば、基本的には何の言語やフレームワークを使っても構わないので、軽くサーバーで使った言語やライブラリなどを紹介しておきます。
言語
- TypeScript
今年から書き始めたから勉強も兼ねて。
フレームワーク
- Express.js
使い慣れてるので。
DB
Herokuにデプロイしたかったから。
ORM
- TypeORM
github.com
昔は業務でSequelizeを使ってたので(最近はmongoDB+mongoose)、Sequelizeにするか迷いましたが、TypeORMがいいらしいとの噂を耳にしたので、今回は使ったことのないTypeORMにしてみました。
デプロイ先
- Heroku
サーバー代を払いたくなかったから。
その他
- openapi-generator
Nature Remoのapiはopenapiの定義書が公開されています。
これをopenapi-generatorという、定義書からサーバーやapi clientを自動生成してくれるライブラリへ食わせてapi clinetは自動生成しました。
https://swagger.nature.global
vironは、node-vironlibというvironのサーバーを立ち上げるために必要になる色々なミドルウェアやcontrollerなどをnode.jsで実装してヘルパーライブラリとして提供していますが、d.tsを提供していないのと、今回そこまでしっかり実装するほどの内容でもなかったので、使わずに全て自前で実装しました。
機能
作った機能を紹介します。
まあ、とりあえずなんでもグラフ化すればカッコよく見えるだろうと思って、Nature Remoから取れる温度、湿度、照度を定期的にNature Remoのapiから取得してPostgreSQLに溜め込んで、その日1日分をグラフ化しました。
上3つのカードコンポーネントは、現在の温度、湿度、照度を表示してます。
工夫した点は、10分おきにNature RemoのAPIを叩いてDBに保存しているのですが、HerokuのPostgreSQLの無料枠はrow数に制限があり、10分おきに行を追加していたらすぐに無料枠を失ってしまうので、1日のデータは1行にして、時間毎のセンサーデータはjson型にして1つのカラムに蓄積するようにしました笑
これでグラフ表示する時も今日の日付のデータを1つ引いて来ればいいので簡単です笑
あとは、とりあえず登録してあるNature Remoデバイスと、それらが管理している家電一覧などを表示できるようにしました。
このくらいの機能は、apiを叩いてちょっとレスポンスを整形して返してあげれば良いので実装は非常に簡単です。
まとめ
今回は、Nature Remoとvironを使って簡単なダッシュボードみたいなものを作ってみました。
Nature Remoのapiには、家電の操作系のエンドポイントも用意してありそうだったので、vironからエアコンの制御とかもできるようにすると、より管理画面らしくなって面白いかもしれないです。
TypeORMを使ってみた感想は、書き方がActive Record風な書き方や、SequelizeのようなData Mapperのような使い方が用意されていたり、スキーマのマイグレーションもできるようになっていたので結構リッチな感じでした。
今回作ったサーバーアプリケーションは、一応GitHubに上げておくので、cloneするなりforkするなりして、煮るなり焼くなりしてください。
github.com
jestのテストが遅い場合に確認すべきこと
こんにちは。
急に寒くなりましたね。
僕は最近iPad Air4 を予約しました。
今日は、javascriptのテストフレームワークのjestについてです。
業務では去年くらいから新規サーバーアプリケーションはjavascriptではなくTypeScriptを使っています。
それに合わせてテストフレームワークもmochaからjestへ移行しました。
jestの特徴の1つは、一意なグローバル状態を持つことを保証しつつ、複数のテストを別プロセスで並列に実行してくれるため、安全に高速なテストをすることが可能なことです。
しかし、僕が前にいたチームでjestのテストにかかる時間がやたら長いことが話題になってました。
今日は、この高速なはずのjestがやたら遅い問題が社内で話題になっていたので調査してまとめてみたので、もしjestが遅くて困っている方は参考にしてみてください。
現状の問題
テストは基本的に、開発者のローカルPC(mac)でコンテナなどは使わずに走らせているのと、gitのサブブランチにpushした際などにgoogle cloud build を使ったコンテナ上のテストの2箇所で走っています。
そのローカルとCIどちらとも非常に遅い状態でした。
今回はこの2つの環境での問題について調査しました。
macでjestが遅い問題
まずは、mac上で遅い問題について調査しました。
実際、僕がいた時も若干遅さを感じていましたが、それからさらにコードとテストが増えていき、テストにかかる時間も比例して長くなっていったようです。
現状、51個のTest Suiteと285個のTestがあります。
タイムは、npx jest --clearCache
でキャッシュを消した状態で実行すると約69秒でした。
Test Suites: 51 passed, 51 total Tests: 285 passed, 285 total Snapshots: 0 total Time: 69.496s
同じコードでは無いので厳密な比較にはなりませんが、Node.jsとmochaで書かれた別のアプリケーションでは、テストの数が1000個を超えてますが30秒くらいしかテストの実行にかかりません。
それに比べるとテストの数が3分の1しかないのに速度が倍以上遅いです。
pprofでプロファイル
最近、node.jsでもpprofが使えるということを知って、プロファイルしてみるのがマイブームなのでとりあえずpprofを試してみました。
参考記事
moznion.hatenadiary.com
会社のコードのため、セキュリティとか色々な関係でプロファイルしたものは載せませんが、testごとの初期化がmaxで2~3秒かかっているものはあったものの、テスト1つ1つに大きな問題はなさそうでした。
メモリもそれぞれのテストでは特別多く使ってる感じはなかったです。
htopでCPUなどの使用率をみる
次にhtopコマンドで、テスト実行中のCPUやメモリーの使用率を確認しました。
ちなみに会社で支給されているマシンはMacBook Pro (15-inch, 2017) で、クアッドコアのIntel Core i7です。(近いうちに新しいモデルにする予定)
こちらがテスト実行中のhtopの画面。
あんまりCPUとか詳しくないですが、hyper threading により物理コアは4つですが、PCからは論理プロセッサを含めて8つのコアがあるように見えているので、htopでの使用率も8個表示されています。
そして物理コア分しか使用率が振り切っていないことがわかります。
続いてこれは、jestで実行中のtestの数です。
7つ同時にテストを走らせていることがわかります。
コードを確認したところ、ここで最大worker数を決めているようです。
https://github.com/facebook/jest/blob/a1adaff21db93517e60be840303518490cd5d51f/packages/jest-config/src/get_max_workers.js#L20
const cpus = os.cpus().length; // 8 return Math.max(argv.watch ? Math.floor(cpus / 2) : cpus - 1, 1);
しかしhtopを見る限り論理プロセッサは全く使われていない様子なので、4プロセッサで7つworkerを管理している状態でした。
そこで、最大worker数を4つに制限して再度テストを実行してみました。
worker数は --maxWorkers
オプションで指定できます。
Test Suites: 51 passed, 51 total Tests: 285 passed, 285 total Snapshots: 0 total Time: 60.062s
結果10秒早くなりました。(キャッシュをクリアした状態)
キャッシュが効いた状態のテストだと30 ~ 40秒くらいになったので、まあ今までよりはストレスフリーになったでしょうという感じです。
考察
結局mochaを使ってるプロジェクトの方が速度が早いですが、mochaで書かれたテストはテストごとに状態が一意ではなく、globalに1度セットアップした状態を共有してるので、最初の初期化が終わってしまえばテストが直列実行でも高速です。(書き方にもよると思いますが)
しかしjestはそれぞれのテストが独立していて、一意なグローバル状態を持つことが保証されていることが売りなため、テスト毎に初期化があってそれに時間がかかってしまうことはしょうがない気がします。
なので、それぞれが分離されて独立していることによる、より正確なテストとのトレードオフなのかなと思いました。
CI上ではさらに遅い問題
一旦ローカルでの速度問題は解決したということで、次にCI上での問題についてです。
CIはgoogle cloud buildを使っていて、コンテナ上でjestが実行されています。
一番遅かった時期が、ちょっと前の話だったのでログがどこにあるか分からなかったのですが、7分くらいはかかっていたそうです。
とりあえずCIと同じよな環境で実行すべく、ローカルにコンテナを立ち上げてテストを実行してみました。
実行中のhtopの画面がこちら。
コアが全部振り切ってて、良さそうに見えてましたが、いつまで経っても一向にテストが進まないので途中で諦めてしまいました。
コンテナの使えるCPU数を減らしたりなど色々試しましたが遅いままでした。
おそらくcloud build上でも同じことが起こってるんだろうと思います。
それで、ググってみたところ公式ドキュメントのトラブルシューティングに、DockerやCIサーバー上でjestが遅い場合の対処法が書いてありました。
どうやら、高速SSDとマルチコアCPUを搭載した今時のPCなら高速だけど、特定のセットアップでは低速になる場合があるようです。
このドキュメントにリンクされているissueにも、circleCIやTravisCIでテストを実行するとlocal pcに比べて遅いことなどが報告されてました。
対応方法としては、 --runInBand
オプションを付けて直列にテストを実行させると早くなるらしいです。
なんと最大50%早くなったとの報告も。
実際にローカルのコンテナでも直列実行したところ、並列だといつまで経っても1個もテストが進まなかったのに対して、直列だとちょっと遅いですが確実に進んでました。
時間も3分ちょっとくらいにはなりました。
Test Suites: 51 passed, 51 total Tests: 285 passed, 285 total Snapshots: 0 total Time: 202.734s
考察
traviceCIなどのVMベースのCIだと、CPUのコア数をみて、適切な worker数を指定してあげれば高速になるそう。
それでも解決しない場合は、 --runInBnad
で直列に実行させるしかなさそうです。
あとは、ちょっと微妙ですが、テストをディレクトリ毎に分けて実行するようにして、cloudbuild上でディレクトリごとのテストを別ステップとしてパラレルに行うとかすれば早くなるかもなと思いました。
まとめ
今回は、ローカルとCI上でテストが遅かったので、原因を探るために色々調査しました。
ローカルの方は、ちょっとパフォーマンス改善できたのでよかったです。
正直CI上で遅い時は、直列実行するしかないってのは、jestの旨味が半減してるようで微妙だなとおもいました。
もっといい方法がないか、今後も調査してみます!
『マイクロサービスパターン』を読んだ
こんにちは。
今日は、マイクロサービスパターンという本を読んだので感想的なメモです。
ちょっと前まで、技術書を電子書籍で読むチャレンジをしてましたが、どうも電子書籍だと続かないので、最近は諦めて物理本を買って読んでます。
マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ
- 作者:Chris Richardson,長尾高弘,樽澤広亨
- 発売日: 2020/03/23
- メディア: Kindle版
買ってから知りましたが、この本の翻訳版らしいです。
Microservices Patterns: With examples in Java
- 作者:Richardson, Chris
- 発売日: 2018/11/19
- メディア: ペーパーバック
読んだモチベーション
僕は1年くらい前から、10以上のマイクロサービスで運用されているサービスの開発に関わっています。
既存のマイクロサービスに機能を追加したり、新しいマイクロサービスを開発していて、あんまりマイクロサービスの実装パターンや設計方法について知らないなと最近思い、もっと色々なデザインパターンを学びたいなと思って、今回この本を読んでみました。
感想
自分の関心がある部分以外は結構端折って読んだので、かいつまんで感想書きます。
マイクロサービス間の通信方法について
3章でマイクロサービス間の通信方法について、同期的な通信と非同期メッセージングの2通りの方法について解説されてました。
普段の業務では、一部で非同期メッセージングを利用してますが、ほぼRESTを使った同期通信を使ってるので、非同期メッセージングの使いどころなどは勉強になりました。
マイクロサービスアーキテクチャでのトランザクション管理
4章でマイクロサービスアーキテクチャでのトランザクション管理について、サーガという非同期メッセージングとローカルトランザクションを使ってデータ整合性を維持するパターンを踏まえて解説されてました。
サーガ自体を知らなかったので勉強になりました。ただ、イベント駆動で非同期的にやり取りするパターンだけで全てのユースケースは網羅できないと思うので、同期的な通信でのトランザクション管理のパターンも知りたかったです。
この本全体で、マイクロサービスでトランザクション管理といえばサーガという感じでサーガ全推しな感じでした。
他のパターンも気になって調べてみましたが、tcc(try/confirm/cancel) というパターンもありました。
engineering.mercari.com
内容を読んでみると、業務で使ってるやりかたがtcc(choreographyっぽい)だと気づきました笑
クエリー実装について
7章でマイクロサービスアーキテクチャでのクエリー実装について解説されてました。
単純に複数サービスからデータを取得して合体させるAPI compositionパターンと、それでは表現できない複雑なクエリーの場合は、専用のテーブルと場合によってはそれ用のサービスを用意して効率よくクエリーを実装しましょうというCQRSパターンについて解説されてました。
普段は、ほぼAPI compositionで頑張ってたので、CQRSについては、勉強になりました。
まとめ
マイクロサービスアーキテクチャでの色々な実装パターンや設計などを知りたかったので、この本はとても丁度良い本でした。
普段の業務で使ってる技術スタックに置き換えて、さらに深堀って勉強していきたいです。
他にもモノリスからマイクロサービスへの移行をするための設計方法やインフラ回りなどについても解説してあるので、そっちの方に興味がある方にもおすすめだと思います。
おわりに
次に読む本は、データ指向アプリケーションデザインにしました。
データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理
- 作者:Martin Kleppmann
- 発売日: 2019/07/18
- メディア: 単行本(ソフトカバー)
結構苦手分野っぽいですが、知りたいことがいっぱい書いてありそうだったので頑張って読みます。
ISUCON10 予選に参加しました
ISUCON10予選に参加したので、今日は参加報告と振り返りです。
昨年が初参加だったので今回が2回目の参加です。
ギリギリまで参加登録してなくて、twitterで残り100チーム切ったとかの報告があって、慌てて参加登録して滑り込みセーフな感じでした。(480番だった)
会社の同期5~6人と今年も出たいねと言っていましたが、僕以外だれも参加登録してなかったため、誰と出るかで揉めるのが嫌だったので、今回はこっそり1人で参加しました。
去年の自分のブログ見ながらやること思い出して作業してたので、今年もやったこと書き留めておきます。
使った言語と結果
昨年は、Goやってみたかったので事前にGoの勉強をしてGoで参加しました。
今年は、慣れた言語がいいなと思って、普段業務で使っているNode.jsで参加しました。
大体過去問みるとkoa.jsが多かった気がしてたので、今年もkoa.jsだろうなと思っていたら、いつも業務でお世話になってるexpress.jsだったので、心理的障壁がかなり少なくてよかったです。
余裕なくて動かしてないですが、今年はDenoの参考実装もあってびっくりしました。
次に結果ですが、471点スタートで、最高スコアは1006点、最終スコアは 998点でした。
ちょっと長いですが、下記がスコアの流れです。
19:00頃まで500点台をウロチョロしてましたが、そこから900点台まで一気に上がりました。
どうでもいいですが、ベンチマーカーを一番最初に回しました笑
やったこと
12:20 ~ 13:00頃
sshでマシンに接続できてからは、node.jsのコードをgithubのprivate repositoryへプッシュしました。
github.com
基本的な作業フローは、手元のPCにcloneしてVSCodeで編集して、ブログにまとめやすくしようと思って、一応プルリク作ってmasterにマージしてました。
masterにマージしたものは、sshでインスタンスに入って手動でpullしてアプリケーションを再起動してました。
13:00 ~ 14:00過ぎ
次にレギューレーション全部読んで、 botからのリクエストは 503 Service Unavailable
を返しても良いと書いてあったので、それを実装することにしました。
ISUCON10 予選マニュアル · GitHub
最初Nginxでやろうと思いましたが、Nginxのconfを書くのになれてなかったので、expressのmieddlewareで書いても対して変わらんだろうと思い、Node.jsで実装しました。
このPRのあとから、色々手直ししましたが大体こんな実装です。
uaをみてbotなら503を返すmiddlewareを追加 by ishikawa-pro · Pull Request #1 · ishikawa-pro/isucon10-qualifying · GitHub
なぜか、459点とスコアがちょっと落ちました。
雑にcurlでしか動作確認してなかったので、コードが間違ってたか、リクエスト数が増えてDBのボトルネックがより目立つようになったかと思いました。
今考えたらちゃんと計測すればよかったですが、後者だと信じて引き戻さずに次の作業へ進みましたw
14:00 ~ 16:00 過ぎ
コード読んだり、NginxやMySQLの設定をみたりしつつ、Nginxのlog formatを変えてalpで解析できるようにしたり、MySQLでスロークエリーログを出すようにしてました。
16:00 ~ 17:00 頃
この変は記憶が曖昧ですが、スロークエリーログを見ながらMySQLにインデックス貼ったりしてました。
確か estate
のrentにindex貼った気がします。
ALTER TABLE isuumo.estate ADD INDEX rent_index(rent);
ここでようやく初期スコアをちょっと超えて、518点とかになりました。
17:00 ~ 17:30
alpとMySQLのログから /api/estate/low_priced
がわりとよく叩かれてるのと、 estateのlow_pricedが変わるタミングは /initialize
時と、csv入稿の時だけっぽかったので、オンメモリ上で管理するように修正しました。
533点と思ったほどには上がりませんでした笑
17:30 ~ 18:00 過ぎ
またalp見直したり、slow query logみたりしてて、nazotteがやっぱり遅いよな〜と思ってコード見てました。
とりあえず下記SQLを、取得したesteteの数だけfor文で実行していたため、非同期処理をシリアルに実行するようになっていたのが気になったので、Promise.allでパラレルに取得できるように修正しました。
"SELECT * FROM estate WHERE id = ? AND ST_Contains(ST_PolygonFromText(%s), ST_GeomFromText(%s))";
これまたなんと、517点とスコアが落ちました。
コードは間違ってなかったのできっと、複数のSQL実行をシリアルからパラレルに修正したことでDBの負荷が上がったんだと信じて、これも引き戻さずに次へ進みましたw
18:00 ~ 19:00
結構お腹が空いてきてました笑
確かこの時間くらいに、nazotte用にestateにインデックスを貼りました。
ALTER TABLE isuumo.estate ADD INDEX lat_long_index(latitude, longitude);
531点とか落ちたスコア分が元に戻るくらいには上がった気がします。
そのあとは、コードを読んでも、極端に複雑なSQLが沢山あるわけでもないし、椅子の購入や資料請求もシンプルだったのでまだボトルネックではなさそうだなと思ってました。
19:00 ~ 20:00
マシンスペックが1コア2GBで、ベンチを回しなが htop
を見てるとメモリはちょっと余裕あるけど、CPUは100%で張り付いてるな〜という感じでした。
まだMySQL自体のチューニングがほぼ手付かずの状態だったので、そっちの方をやるかという感じになって色々ググってました。
最終的にthred_cache_size, query_cache_size, innodb_buffer_pool_sizeなどをいじりました。
query_cache_sizeとquery_cache_limitはどれくらいが適切がよく分からなくて感覚で設定しました。
この辺は来年までに詳しくなりたい。
thread_cache_size = 151 query_cache_limit = 50M query_cache_size = 512M query_cache_type = 1 innodb_buffer_pool_size=1G innodb_log_file_size=250M
この設定がすごく効いて、一気に900点台までアップして、色々微調整して1006点まで行きました。
このスコア見たときは声出ましたw
19:00過ぎで、たしか順位が真ん中くらいだったのが一気に61位まで上がったので結構テンションあがりました笑
結局パラメータいじり過ぎて、一番よかったパラメータが分からなくなり977点で着地。
ちゃんといいスコア出たときは、記録せねばと猛反省しました。
20:00 ~ 21:00
残り時間で何ができそうか考えて、インスタンスを1台しか使ってなかったので、1台目をMySQL専用機にして、とりあえず2台目をアプリケーション用サーバーにしてみようと思いました。
しかし、MySQLに繋ぐためにポートを開けないといけないが、この時間に焦って慣れないことしてインスタンスをダメにしたらヤバイなと思って諦めました。
来年までにLinux力も絶対上げたい。
最後の最後に /api/recommended_estate/:id
でindex貼れそうだと気付いて、
ALTER TABLE isuumo.estate ADD INDEX door_index(door_height, door_width);
を貼ったら998点になり、slow query logの設定とかを無効にして終了。
後日公開された参考値も同じ998だったので再起動試験も問題なしでした。
感想
昨年は3人で出て、簡単な N+1を直して微妙にスコア上がって終了だったのが、今年は1人参加で初期スコアから倍以上アップして、後半に一時61位まで上がったので、1年で成長をすごく実感できてよかったです。
結局ラストスパートでめちゃくちゃ抜かれていったみたいで悔しかったので、来年こそは決勝いけるように頑張ります。
最後に、運営の皆様、お疲れ様でした。
決勝出場するチームの皆さんは頑張ってください!
chromeのreferrer-policyについて
[9/8追記]
chrome 85がリリースされたのに、defaultのreferrer-policy変わってないなと思ったら段階的に変わっていくみたいです。
A new default Referrer-Policy for Chrome: strict-origin-when-cross-origin
こんにちは。
業務でchromeのreferrer-policyについてちょっと調べることがあったので、今日は軽くまとめです。
referrer-policyとは
そもそも僕が、referrer-policyについてそんなに詳しくなかったのでまずは簡単な説明。
Referrer-policyは、リクエスト時にreferer headerにどれくらいreferer情報をつけるかを設定するためのヘッダーです。
refererの情報量とは、具体的に
- origin, path, query 全て
- originだけ
- 情報なし
の3パターンがあります。
referrer-policy headerに付けられる値は、
- no-referrer
- no-referrer-when-downgrade
- origin
- origin-when-cross-origin
- same-origin
- strict-origin
- strict-origin-when-cross-origin
- unsafe-url
があります。
それぞれの値がどういう振る舞いをするかなどの詳細はMDNがよくまとまっています。
referer
の正しいスペルは referrer
ですが、RFCの標準化時にスペルミスしたものがそのまま登録されたという話は、 Real World HTTP で読んだ気がします。
referrer-policyの方は、後からできたので正しいスペルになってるそうです。
chromeのdefault値について
現在のchromeは、 デフォルトのreferrer-policyが no-referrer-when-downgrade
になっています。
no-referrer-when-downgrade
は、https => https や http => https などのセキュリティ水準が同一または改善される場合は、クエリなどの全ての情報が送信されますが、https => http のように水準が低下する場合は、refererを送信しない挙動になっています。
そしてここからが本題です。
chromeのdefaultのreferrer-policy が version 85から strict-origin-when-cross-origin
へ変わるそうです。
www.chromestatus.com
strict-origin-when-cross-origin
は、同一オリジンの場合はクエリを含む全ての情報を送信し、クロスオリジンの場合はoriginのみを送信、 https => httpへのセキュリティレベルの落ちる通信では情報を送信しません。
僕は、会社のPCのデフォルトブラウザをMicrosoft Edgeのカナリアリリース版を使ってるのですが、EdgeはCanary v79と Dev v79から反映されていました。
stable版に、いつ反映されるかは確認できませんでした。(version 83.0.478.64現在は no-referrer-when-downgrade
でした)
docs.microsoft.com
今回、業務で開発してるアプリケーションでreferer headerを使っている部分があり、カナリアリリース版のEdgeでアクセスした場合に、挙動がおかしかったので気付きました。
ちなみに、僕の試した環境だとサブドメインが同じ場合でもクロスオリジンとみなされていました。 (originしか入っていなかった)
おわりに
今回は、referrer-policyについてと、chromeのreferrer-policyのdefaultが変更されることについてまとめました。
普段、カナリアリリース版のEdgeを使っていますが、今回初めて業務の役にたったきがします笑
ベータ版とかを積極的に使っていくこともやっぱり大事ですね笑
御拝読ありがとうございました。
TypeScriptで指定したfieldの値をキーとしたオブジェクトを作る
こんにちは。
珍しく1週間ただずの更新です。
最近はTypeScriptを結構書いていて、ちょっと知見もたまったのと、仕事が一段落してちょっと時間ができたので、今日はTypeScriptについて書きます。
指定したfieldの値をキーとしたオブジェクトを作る
普段の業務ではMongoDBを使っていたりする関係で、2つのコレクションから取ってきた複数のドキュメントをコードで、特定のfiledをキーにしてジョインしたいということが、たまにあります。
const books = [ {author_id: 1, title: 'Coloring for beginners'}, {author_id: 1, title: 'Advanced coloring'}, {author_id: 2, title: '50 Hikes in New England'}, {author_id: 2, title: '50 Hikes in Illinois'}, {author_id: 3, title: 'String Theory for Dummies'}, {author_id: 5, title: 'Map-Reduce for Fun and Profit'} ]; const authors = [ {id: 1, name: 'adam'}, {id: 2, name: 'bob'}, {id: 3, name: 'charlie'}, {id: 4, name: 'diane'} ]; // ジョインしたい cosnt joined = [ {author_id: 1, title: "Coloring for beginners", name: "adam"}, {author_id: 1, title: "Advanced coloring", name: "adam"}, {author_id: 2, title: "50 Hikes in New England", name: "bob"}, {author_id: 2, title: "50 Hikes in Illinois", name: "bob"}, {author_id: 3, title: "String Theory for Dummies", name: "charlie"}, {author_id: 5, title: "Map-Reduce for Fun and Profit", name: undefined}, ]
ジョインするアプローチも色々あると思いますが、普段Node.jsでは、ジョインしたいドキュメントの配列から特定のフィールドの値をキーとしたオブジェクトを作成して、もう一方のドキュメントの配列をmapで回してジョインしていく、という方法を使ってました。
// { "1": {id: 1, name: 'adam'}, "2": {id: 2, name: 'bob'}, ...} のようなオブジェクトを作る const mapedObject = authors.reduce((ret, v) => { ret[v.id] = v; return ret; }, {}); const res = books.map(b => { // bookのauthor_idを使って、紐づいているauthorを取得 const a = mapedObject[b['author_id']]; return { ...b, name: a ? a.name : undefined // left join的な感じ } })
特定のキーでmapedObjectを作るのは、色々なところで使えるので
const toMap = (arr, key) => { return arr.reduce((ret, v) => { ret[v[key]] = v; return ret; }, {}); };
という感じで関数化してます。
今日はこれをTypeScript化するときに考えたことのまとめです。
さっきのNode.jsのコードをそのままTypeScript化しようとすると
const toMap = <T>(arr: T[], key: string):{[k: string]: T} => { return arr.reduce<{ [k: string]: T }>((ret, v) => { ret[v[key]] = v; return ret; }, {}); };
こんな感じにできそうですが、以下のようなエラーがでてコンパルが通りません。
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'. No index signature with a parameter of type 'string' was found on type 'unknown'.
v[key]
で T
から値を取り出そうとしますが、 Tのindex signatureが宣言されていないので unknown型
になっており、v[key]
のように直接的に値をとることができないためです。
この問題は、Tのindex signatureを宣言することで解決できます。
const toMap = <T extends {[k: string]: any}>(list: T[], key: string):{[k: string]: T} => { return list.reduce<{ [k: string]: T }>((ret, v) => { const a = v[key]; ret[a] = v; return ret; }, {}); };
T extends {[k: string]: any}
とすることでTのindex signatureをstringと宣言したのでコンパイルは通るようになりました。
しかし、keyがただのstringだとTが持っていないキーを指定できてしまうのがイケてないので、keyof を使ってもうちょっと工夫してみます。
const toMap = <T extends {[k: string]: any}>(arr: T[], key: keyof T):{[k: string]: T} => { return arr.reduce<{ [k: string]: T }>((ret, v) => { ret[v[key]] = v; return ret; }, {}); };
第2引数の型を keyof T
とすることで、Tが持つものしか渡せなくなります。
また、こうすることでエディタもサジェストしてくれるようになります。
Map Objectを使う
上記の方法でもいいと思いますが、Map Object版を考えて見ました。
const toMap = <T, K extends keyof T>( arr: T[], key: K ): Map<T[K], T> => { return arr.map<[T[K], T]>(e => [e[key], e]) // ['キーにしたい値', T] のtupleを作る .reduce<Map<T[K], T>>((acc, [k, v]) => { // Map Objectを作成 & set して returnする。 acc.set(k, v); return acc; }, new Map<T[K], T>()); };
Map Objectを使うと、index signatureを気にしなくて済みます。
また、Map Objectの方が entries()
, keys()
, values()
などのメソッドがあり、個人的にはobjectよりも色々扱いやすいのでMap Objectを使うのにハマってます。
まとめ
もうちょっと、色々なtipsを書こうと考えていましたが、書いてみると分量が多かったので今日はこれだけにします。
また時間があれば、ブログにまとめてみます。
最後までご拝読ありがとうございました。
Express.jsでasync/awiatをいい感じに使う
はじめに
お久しぶりです。
最近は業務でTypeScriptを書き始めたりしてました。
今日は、会社のテックブログに載せるつもりで書いてみたけど、内容的に会社のブログで載せるほどの内容にならず、ボツにした記事をここで供養しようと思いますw
ちなみに、会社のテックブログに書いた内容はこちら 👇
https://cam-inc.co.jp/p/techblog/407753782164718758
Express.jsのTIPS紹介
ということで今日は、Express.jsのtipsを1つ紹介します。
Express.jsとは
まず簡単にExpress.jsの説明です。
Express.jsは、Node.js製の薄いWebアプリケーションフレームワークです。
https://expressjs.com/ja/
とりあえず、これだけ書けばサーバーを動かすことができます。
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(port, () => console.log('started server'));
上記コードだと、 /
というエンドポイントでGETメソッドのリクエストを受け取ると、5行目の関数のコールバックメソッドが呼ばれて、 Hello World!
という文字列がリクエストのレスポンスとして送られてきます。
Express.jsでの非同期処理
先ほどの例では、リクエストが来た時に文字列を返すという同期的な処理しかありませんでした。しかし、実際のアプリケーションではDBに問い合わせたり、別のアプリケーションと通信をしたりなど、非同期的な処理が発生すると思います。
Node.jsでの非同期処理のアプローチには大きく分けて、コールバックパターンとPromiseを使ったパターンがあると思いますが、最近はPromiseで非同期処理を扱うことの方が多いと思いますので、今回はPromiseを使った例のみ扱います。
// ユーザー一覧を返す app.get('/users', (req, res, next) => { // DBに問い合わせてUser一覧を取得して返す。 Users .find() .then(users => { res.json(users); }) .catch(error => { next(error); }); }); // ExpressにビルトインされているError Handlerのmiddleware // next()にエラーを渡すことで、このmiddlewareが呼ばれる app.use((err, req, res) => { console.error(err); res.status(500).send('Internal Server Error'); // ステータスコード500でレスポンスを返す });
上記のコードは擬似的なコードですが、 /users
というエンドポイントをGETでリクエストすると、非同期処理としてDBに問い合わせてuser一覧を取得してレスポンスとして返しています。
最後の4行は、Express.jsにビルトインされているエラーハンドラーのミドルウェアで、発生したエラーをここで処理します。
ここで大事なのは、 Users.find()
の最後に catch
でエラーをハンドルして next
に渡していることです。Express.jsは同期処理で発生するエラーはキャッチして扱ってくれますが、非同期処理に関してはよしなに扱ってくれません。
Promiseを返す関数を使う場合は、catchでエラーを受け取り next
に渡さないと非同期処理内でエラーが発生した場合に、エラーがハンドルできません。
async/awaitの場合
先ほどはPromiseを使って非同期処理を書きましたが、次はPromiseをより便利に扱う async/await
を使った書き方です。
app.get('/users', async (req, res, next) => { // DBに問い合わせてUser一覧を取得して返す。 try { const result = await Users.find(); res.json(result); } catch (error) { next(error); } }); // error handlerの処理は同様なので省略
注意すべき点は、 async/awat
も非同期処理なので、try/catch
でエラーをcatchして next
にエラーを渡してやる必要がある点です。
tips
async/await
は then
の呼び出しがないため、同期処理と同じようにネストせずに書ける便利な構文ですが、上の例で書いたようにExpress.jsのハンドラーで使うには、必ず最初に try/catch
が必要になってしまいます。
そこで今回のTIPSの紹介です。
以下のような async/await
をシンプルに書けるようにwrapper関数を定義します。
const asyncWrapper = fn => { return (req, res, next) => { return fn(req, res, next).catch(next); } }; app.get('/users', asyncWrapper(async (req, res, next) => { // DBに問い合わせてUser一覧を取得して返す。 const result = await Users.find(); res.json(result); })); // error handlerの処理は同様なので省略
最初の行で定義している asyncWrapper
は、引数に AsyncFunction
を受け取り、戻り値としてExpress.jsのハンドラーを返しています。そして、実際にリクエストがあった際にハンドラー内部では、 AsyncFunction
である引数 fn
を実行しており、 catch
でエラーをハンドルして、 next
にエラーを渡しています。
リクエストハンドラーで使う際は、 async/await
の例で書いたハンドラーを asyncWrapper
の引数として渡します。最初の例で書いていたコードでは、ハンドラー内部で try/catch
を書いていましたが、その役割は asyncWrapper
が引き受けてくれているので、ハンドラー内部で try/catch
を書かなくてもよくなり、コードがスッキリしました。
Express.js 5では
最後にExpress.js 5ではどうなりそうかについてです。
Express.js 5からは、今までExpress.js内で書かれていたコードは、 pillarjs
というprojectのコンポーネントに移行しています。それと合わせてHTTP/2とPromiseを返すハンドラのサポートもしているようです。
今までのルーティングやハンドラのmoduleは、 pillarjs
の router
というリポジトリに移行されており、v2.0.0-beta.1 からpromiseを返すことが可能になっています。
https://github.com/pillarjs/router
まだ先になりそうですが、Express.js 5からは asyncWrapper
のようなコードは不要になるかもしれませんね。
まとめ
今回は、Express.js のハンドラーで非同期処理を扱い方とtipsを紹介しました。
ボツにした記事も供養できて満足です笑
またいいネタがあれば記事にしたいと思います。