【SQL】月別で集計するときに値がない月は0と表示したい

SQLに関するメモ。前提としてPostgreSQLを使っています。

以下のようなTimestamp型で日時の情報を持つテーブルがあるとする。

id: integer timestamp: timestamp with time zone
1 2022-01-01 00:00:00+00
2 2022-02-01 00:00:00+00
3 2022-10-01 00:00:00+00
4 2022-10-01 00:00:00+00
5 2022-12-01 00:00:00+00

で、月ごとの件数を集計をしたい場合、何も考えずにこんなSQLを書く。

SELECT to_char(timestamp, 'YYYYMM') AS MONTH,
       count(id)
FROM table1
GROUP BY MONTH
ORDER BY MONTH;

そうすると得られるのはこんな結果。

month count
202201 1
202202 1
202210 2
202212 1

これはこれで正しい。正しいが0件の月も表示したい。つまりこんな結果がほしい。

month count
202201 1
202202 1
202203 0
202204 0
202205 0
202206 0
202207 0
202208 0
202209 0
202210 2
202201 0
202212 1

そんなときはgenerate_series()を使う。generate_series()ってのは文字通り連続値を生成できる関数。実はこれ日付型でも使える。

こんな感じ。

SELECT *
FROM generate_series('2022-01-01'::timestamp, '2022-12-01', '1 month');

実行するとこういう結果が得られる。

| generate_series     | 
| ------------------- | 
| 2022-01-01 00:00:00 | 
| 2022-02-01 00:00:00 | 
| 2022-03-01 00:00:00 | 
| 2022-04-01 00:00:00 | 
| 2022-05-01 00:00:00 | 
| 2022-06-01 00:00:00 | 
| 2022-07-01 00:00:00 | 
| 2022-08-01 00:00:00 | 
| 2022-09-01 00:00:00 | 
| 2022-10-01 00:00:00 | 
| 2022-11-01 00:00:00 | 
| 2022-12-01 00:00:00 | 

というわけでこの結果と元のテーブルをジョインする。そうすると期待したものが得られる。次のようなSQLになる。

SELECT
       t1.month,
       count(id)
FROM
  (SELECT to_char(g, 'YYYYMM') AS MONTH
   FROM generate_series('2022-01-01'::timestamp, '2022-12-01', '1 month') AS g) AS t1
LEFT JOIN
  (SELECT id,
          to_char(timestamp, 'YYYYMM') AS MONTH
   FROM table1) AS t2 ON t1.month = t2.month
GROUP BY t1.month;

年月だけで集計しているのでto_charがあってちょっと見づらいけど。やっているのは先のようにgenerate_seriesで年月のデータを作ったもの(t1)と集計したいテーブルの日時を年月にしたもの(t2)をJOINして、年月でGROUP BYしている。

| month  | count | 
| ------ | ----- | 
| 202201 | 1     | 
| 202202 | 1     | 
| 202203 | 0     | 
| 202204 | 0     | 
| 202205 | 0     | 
| 202206 | 0     | 
| 202207 | 0     | 
| 202208 | 0     | 
| 202209 | 0     | 
| 202210 | 2     | 
| 202211 | 0     | 
| 202212 | 1     | 

というわけでできた。whereで条件指定をする場合は絞り込むのを集計後の結果に対してなのか、集計する対象に対してなのかだけ気をつけること。

MapboxのDirections APIを試してみる

メモです。

Mapboxというのは地図情報のサービス。地図の点だけで言えばGoogle Mapとかと同じようなものと言えば同じようなものなのだけどカスタマイズ性の高さとビジュアライズ周りに強みがあると思っている。なので地図をベースにしたいろんな表現が可能になっていると言える。実績も多く有名なところだとYahooカーナビなんかはMapboxを利用している。

www.mapbox.jp

Mapbox自体の細かい話はさておき、そんなMapboxを使って経路探索をやってみる。つまり出発地、経由地、目的地を指定してルートを作成するやつ。

Mapboxでこれをやる場合にはDirections APIというものを使う。Directions APIというのは端的に言うと経路探索とターンバイターンの情報などを返してくれるAPI。なお、モバイルの場合はこの上にNavigation SDKというのがあってカーナビみたいなUIとかを組み込むことも簡単にできるようだ。

今回はこのAPIを使って複数地点を通るルートを生成するっていうごく基本的なことを試してみる。

といってもとても簡単で基本的には始点と終点の座標を指定して用意されているAPIを叩くだけ。基本は以下のAPIとなる。

https://api.mapbox.com/directions/v5/

APIのリクエストにはAccess Tokenが必要になるがその詳細は割愛する。ここから新たに作るなどして取得してほしい。何も難しいことはない。

Account | Mapbox

これに対していくつかパラメータを指定してリクエストする。今回指定している以外にも無数にオプショナルなパラメータがあるので詳細は公式ドキュメントを確認するのがいい。

Directions | API | Mapbox

まずはプロファイルを指定する。プロファイルってのはルート作成するにあたって自動車のルートか自転車のルートかなど。これは利用できるものが事前に決められている。つまり、車で東京駅から渋谷駅までのルートを作る場合は以下のようなAPIリクエストを行う。

座標については経度(longitude)、緯度(latitude)をカンマ区切りにしたものをセミコロン区切りで渡す。ナビゲーションに使うturn by turnの情報を得るにはオプションパラメータのstepsをtrueにする。

 curl 'https://api.mapbox.com/directions/v5/mapbox/driving/139.76712479870113,35.68141047521832;139.70159288335694,35.658312833125805?geometries=geojson&steps=true&access_token=<MAPBOX_ACCESS_TOKEN>'

レスポンスはかなり長くなるので本投稿では割愛。なお、ここではGeoJsonを指定しているけど実際にはレスポンス全体がGeoJsonフォーマットなわけではなくて、geometry以下だけがGeoJsonフォーマットなようだ。

レスポンスを扱うのに少しだけ知っておく概念がある。それが以下だ。

  • leg 出発から目的地までのルート。入力座標より1つ少ない数になる。つまり、始点と終点の2点だけならlegは一つ

  • step legにおける道のりを示す。複数ある。曲がり角とかそういう案内の数分あると思えばいい?ちなみに道の名前なんかもnameで取れる。intersectionsには途中の交差点の情報が配列で入ってる

  • maneuver 行き方とか。いわゆる「この先の交差点を右折」とかああいうの

構造として、routeが最上位にあるので例えばstepの情報はroute[0].legs[0].stepsに配列で入っている。さらに言うとsteps以下の各要素のmaneuver.instructionでturn by turnのinstructionを取れる。

基本はこれだけ。というわけで今回はここまで。

我が家の板壁化計画

はじめに

近頃Zoomとかで話をするときに背景について聞かれることが増えてきたので過去に別ブログに書いた記事をアップデートしつつこちらのブログに持ってきました。背景と言ってもバーチャル背景ではなくリアル背景なのですが、その話です。

時期的には2020年くらいの話です。

我が家っていうか自室です。もともと7畳ほどの部屋を自室として使っていたのですが、まあなんの変哲もない部屋だったんですね。普通に白い壁紙が貼られてる感じのよくある普通の部屋です。

リビングは一部の壁面をエコカラットにしていてとてもいい感じなんですが。

なので以前から部屋の雰囲気を変えたいなーと思っていたわけです。そんな中で新型コロナウィルスが流行してリモート会議・ビデオ会議が多くなってきてました。Zoomのようにバーチャル背景がある場合はいいんですが、当時在籍していた会社ではその機能がないミーティングツールを使うことが多かったのです。なので部屋の中が見えるんですね。なお、最初の頃はぼかしの機能もなかったはず。

また、Zoom等のバーチャル背景であっても人との輪郭部分がボワッとなることも多くこれも気に入りませんでした。

というわけでバーチャル背景がダメならリアルは背景を作ればいいじゃんとなって以前からやりたいと思っていてできなかった板壁にするってのをやってみました。

ただ、板壁にするって言っても選択肢としては板壁風壁紙を張るパターン、実際に板を張るパターン、その他なんらかの方法でやるパターンがあります。

で、今回は実際に木の板を張ることにしました。なぜかというと質感の問題もありますが、それだけでなく単に壁を板にするだけでなくてそこに棚板作ったりいろいろしたかったからですね。

ラブリコ

さて、実際に板を張る場合、一番簡単なのは壁に直接張るパターンです。でも僕は飽きたときのために壁に直接張るのは嫌だったんですね。いくら持ち家とはいえ取り返しがつかなくなるのは嫌だな、と。

というわけでラブリコを使ってやることにしました。ラブリコ以外にもディアウォールとか同じようなのはいくつかあるんですが見た目とかの観点で今回はラブリコにしています。

ラブリコってのは2×4材用突っ張りブラケットです。要は天井の高さに合わせた2x4の木材を買ってきてラブリコをかぶせて突っ張ることで柱にするのです。その柱に対して板を張っていくことにしたので実際に壁に直接張る必要がありません。

色もいくつかあります。あと、2x4ではなく1x4を使えるものもあります。今回は強度などを考慮して2x4を使うタイプにしました。

これにワンバイ材の板を張ります。なお、当初は内装材とか張ることも考えました。あとはパンチングボード。これらだと施工が圧倒的に楽そうだったので。でも、作った板壁にいろいろ付けたかったこともあり強度的なことを考慮して、ワンバイ材の板を張る決断をしました。

ワンバイ材・ツーバイ材とは

先ほどの1x4、2x4ってのはそれぞれワンバイ材、ツーバイ材という規格の木材です。ワンバイ材とは厚さ19mmの木材のことです。1×1と表記されている場合は『ワンバイワン』と読み、19mm×19mmの木材になります。そんな感じで以下のような種類があります。

  • 1×1(19mm × 19mm)
  • 1×2(19mm × 38mm)
  • 1×3(19mm × 63mm)
  • 1×4(19mm × 89mm)
  • 1×6(19mm × 140mm)
  • 1×8(19mm × 184mm)
  • 1×10(19mm × 235mm)

同様にツーバイ材もあり、その場合は厚み38mmの規格になります。これらはホームセンターに行けば普通に買えます。長さは決まっていないので買ってきて自分で切るか、多くのお店ではその場で加工してもらう、もしくは加工するスペースを借りられることがほとんどだと思います。また、いくつかのサイズではあらかじめカット済みで売られていることも多いです。

ヴィンテージ感を出す

買ってきたSPFの木材は普通に白っぽい木の風合いです。もちろんそのまま使うこともできますが、今回は雰囲気の問題から古材っぽい感じにしていきます。というのももともと使っているサイドボードが古材のものなので雰囲気をあわせたい。

というわけで、アンティーク感やヴィンテージ感を出すのにあたって定番とも言えるブライワックスを使います。この手の定番には今回利用するブライワックスとワトコオイルの2つがあるんですが、それぞれ多少違いがあります。簡単に言うとブライワックスは文字通りワックスで、ワトコオイルは植物油ベースのオイルフィニッシュですね。

どちらも塗るだけですし、重ね塗りすることでより深みを出すこともできますがワトコオイルは乾かすのに時間がかかるんですね。というわけで今回はブライワックスのみで加工することにしました。色はこちらも定番ジャコビアンです。

塗るにあたってはスチールウールを使います。布でもできますがスチールウール推奨です。スチールウールも自分はこういうのを使っています。

また、ワックスなので塗ったあとに磨き上げる必要があります。これは普通のウェスでもTシャツでも何でもいいかと思います。僕は家にあったウェスと捨てようと思っていたタオルを使いました。

作業開始

ここからは適宜当時の実況ツイートを引用していきます。

もともとはこんな感じです。この壁が一面板壁になる予定。実際には両サイドにスペースがあって横4メートルくらいです。

まずは木材を調達しに言ってきたのですが、壁一面用となると結構な数が必要でした。まず柱となる2x4を5本。そして板壁となる横板として1x4をいっぱい。実際には6フィートのものをカットしてトータルで96枚ほどです。

 加工する枚数が多くて一部の受け取りが翌日になりました。

 そして塗っていきます。

作業台はブラック・アンド・デッカーのやつで数年愛用しているやつ。使わないときは折り畳めるのでよい。

 使っているサンダーはリョービの安いやつです。

一通り塗ったら今度は家の中での作業です。部屋を養生するのですがマスカーを愛用してます。

これめっちゃ便利。この緑色っぽいのがガムテープになっててそこを張ってビニール部分を広げるだけで養生完了です。今回みたいな大掛かりな作業じゃないので部屋でやってしまいたいときとかにも重宝します。なお、その後デスクも自作したのですがそのときは天板を塗ったあとに長時間乾かす必要があったために部屋でやりました。このときもマスカー大活躍。

まずは1柱分を張った。張り方はシンプルにネジで打ち込みます。

とても1日で終わる分量ではないので毎日仕事の合間のスキマ時間を使って仕上げていきました。

というわけで残り1列を残すばかりに。

ちなみにこの作業をやるためにかなりの数のネジで止める必要があったので新たにインパクトドライバも買いました。マキタのコードレスのやつです。

もともとboschの小型電動ドライバを持っていたのでそれと2台体制です。

インパクトドライバにドリルビットをつけて下穴をあけて、boschのドライバでネジ締める感じです。

そんなこんなで大体完成しました。

『大体』といったのには理由があります。それは今回使ったラブリコでもディアウォールでもそうなんですがアジャスターの部分がむき出しで見えるので気になってくるわけです。これをどうやって隠すか。

こんな感じの部分をどう隠すかが大事(写真がなんか暗い…)。

結局、僕が取った方法は一番上段の板から天井までのサイズの横板を用意して一番上段から張る方法を取りました。なので一番上段だけ2枚張りになってるんですが、アジャスターが見えてるよりはかなりマシ。

というわけで以下がその完成形です。完璧。

完成

というわけで

これが、

こうなった

棚板とか作っていった結果、今はこんな感じです。

最後に

なんだかんだで2週間ほど(土日除く)かけて、こんな感じでひとまず完成。Zoom映えもよくいい感じです。なお、リモート会議での見栄えのためだけにWebカメラとしてミラーレスのカメラも買った。

必要に応じて棚板を取り付けたりが簡単にできるのがわざわざ板を張ったメリットともいえます。

ちなみにかかった費用は細かくは覚えてないけど全部トータルで5万円弱くらい?そんなにもいってないかも。ネジやブライワックスとかも含めての金額です。

次は横の壁が白いままなので大きいパンチングボードを置いたり、レンガ張っていくつもりだったけど2022年11月現在もこれはできていない。

ただし、途中で言ったようにデスクを自作したり、自室のローテーブルもこのときの余った板で自作したりしている。なお、この2つはブライワックスだと色移りしてしまうので時間はかかるけどワトコオイルを塗って、クリアペーストグレーズで耐水性を与えている。

store.shopping.yahoo.co.jp

これは油性のニスなんだけどこれを塗ることで耐水になる。テーブルやデスクはコップを置いたりすることもあるのでこうした。

また、今年の春に購入した折りたたみ自転車を自室に置くときの台もあまりの木の板を組み合わせて自作した。こっちはブライワックスだけ。

なお、デスクの正面はこんな感じで木の板のパンチングボードを貼り付けている。

次はずっとやりたいと思っているグリーンウォールを自作してどこかの壁をこれにしようと思ってる。ボタニカルな感じにしたい。

Kindle10周年記念セールで見つけた定番本たち

現在、Kindle本ストア10周年キャンペーンで最大50%OFFという大規模セールが進行中です。明日(10/25)までです。

amzn.to

というわけでその中から自分が読んだ本、持ってる本でいいんじゃないのっていうおすすめ書籍をざっと紹介します。

1冊ですべて身につくHTML & CSSとWebデザイン入門講座

  • 僕みたいなHTMLとCSSに苦手意識のある人がなんとなく読むのにとてもいい
  • とはいえ前半は入門編なので実際には4章以降の定番レイアウトをどう実装するかってところくらい

AWSコンテナ設計・構築[本格]入門

  • 佐々木さん他NRIの知人たちによる書籍
  • AWS上でのコンテナに関する本としては決定版的だと勝手に思ってます
  • AWSでコンテナワークロード動かすことに特化した本

図解 Amazon Web Servicesの仕組みとサービスがたった1日でよくわかる

  • これもNRIの方々による執筆
  • 以前ツイートした本同様に網羅的にAWSについて知れる本
  • 1日でわかるかどうかは疑問だけどw

SCRUM BOOT CAMP THE BOOK【増補改訂版】 スクラムチームではじめるアジャイル開発

  • 有名なやつですね
  • 7年ぶりに改訂された版
  • スクラムとはってところからそれをどうチームに導入していくかっていう部分がストーリー仕立てで解説されているので初めて学ぶのにいいと思う

LeanとDevOpsの科学[Accelerate] テクノロジーの戦略的活用が組織変革を加速する impress top gearシリーズ

  • ITエンジニア本大賞2022の技術書部門ベスト10入りのやつ
  • といってもそんなに新しい本ではない

体系的に学ぶ 安全なWebアプリケーションの作り方 第2版[固定版] 脆弱性が生まれる原理と対策の実践

  • 徳丸先生のド定番本
  • 物理本を持ってる人も多いのでは?そんな人はこういう機会にKindle版書い直すのもありかと(自分はそうした)

プログラマの数学 第2版

  • わかりやすく平易な言葉で書かれてるのですらすら読めるし、とても良い復習になる
  • 最後には機械学習についても簡単に書かれてる
  • 数学のごく基本的な部分を知る・復習するにはとてもいい

世界で闘うプログラミング力を鍛える本 コーディング面接189問とその解法

  • コーディングインタビューの対策本ということで少し話題になったやつ

Java言語で学ぶデザインパターン入門第3版

インフラ/ネットワークエンジニアのためのネットワーク技術&設計入門 第2版

  • ソフトウェアエンジニアでもこの辺りある程度押さえておくのは重要だと思うので復習にどうぞ

Winny 天才プログラマー金子勇との7年半

  • 日本のソフトウェア業界における悲劇について書かれた本
  • こういう天才と呼ばれるソフトウェアエンジニアが日本でも輩出されてるってことは嬉しいし、天才であっても最初のバージョンがバグだらけってのは僕みたいな凡人には勇気をもらえる
  • 一方で話自体は切なく、読後なんとも言えない気分になった

Web APIの設計

  • APIデザインについて網羅的にかかれている
  • 網羅的なので自分がデザインするときの考慮漏れとかを防ぐのにはいい
  • あとはデザインにおける考え方の参考にも
  • 一方であくまでもAPIデザインに関する本であり実装サンプルとかはほとんどない

パケットキャプチャの教科書

  • 良くセールになってる定番のやつ
  • パケットの気持ちを知りたい方にどうぞ

令和時代のページネーションを考える (REST API編)

今回はバックエンドAPIでページネーションをどうやるかについての話なので、よくある無限スクロールUIのようなフロントエンド側の実装に関する話はしない。あくまでもAPI、もっと言えばRESTfulなAPIのリクエスト・レスポンスにおけるページネーションの話。

本気で深く考えるというよりざっくり検討したときの話です。

はじめに

REST APIを実装するにあたってリスト系のAPIを提供する場合に必須といっても過言ではないのがページネーション。大量のリソースをレスポンスする場合にそれらを一気に返してしまうことは応答速度、転送量、クライアントサイドでの扱いづらさなどなどに繋がるので必須と言える。 最近、新たなAPIを開発するにあたってページネーションをする必要があったこともあり、今回はこのページネーションをどうやって提供するか整理して改めて検討してみた。

前提

一応前提を書いたものの、言語やフレームワーク固有の話にはあまり踏み込まず汎用的な話だと思う。

なお、今回のようにNest.jsを使っている場合はそのものずばりなライブラリが用意されていたりする。

nestjs-paginate - npm

さらにTypeORMとの組み合わせならばこんなのもある。

nestjs-typeorm-paginate - npm

これらについては詳細も見ていないし、使ったこともないので今回は紹介するにとどめておく。

実装パターン

さて、まず最初に思いつく実装パターンをいくつかあげてみる。他にもあるかもしれないが多くはこれらのパターンに大別されるのではないか。知らないだけで他にもあるかもしれない。

パターンとして大まかにリクエストとレスポンスのそれぞれでこのように分けられる。

リクエス

  • LIMITとOFFSETを指定する方式
  • ページ番号方式
  • カーソル指定方式

そしてこれらをクエリパラメータとして表現するかリクエストボディとして表現するかに分かれる。ただし、基本的にはクエリパラメータで表現することのほうが多いと思われる。とはいえこれはそもそものGETリクエストをどう処理しているかによるだろう。例えばGETのクエリ条件が複雑だったりで最大長を超えるケースなど(ただし、RFC的にはURLの最大長は定義されていなくてブラウザなどの実装依存だったはず)はボディに含めるしかないはず。

LIMITとOFFSETの方式はあくまでも指定したオフセットから件数を制限して取得しているだけなのでページネーションというかは微妙なところではある。ただし、実装としては一番楽で、なぜならこれはそのままSQLに当てはめられるのだ。この方式の場合は基本的にリクエストされた値をそのままSQLのLIMITとOFFSETに渡すだけで基本的には成立する。

また、この場合は後述するレスポンスへのメタ情報の格納も必要ないだろう。

ページ番号方式はわかりやすいし事前に用意もしやすいが、新しいデータが追加されたり削除されたりしたときに取得結果が重複してしまうといった意味では一貫性がないとも言える。

極端な例だがページサイズが10件だとした場合に、2ページ目にアクセスした後に先頭に10件追加されてしまうと次のページである3ページにアクセスしても先にアクセスした2ページ目の内容と同じ内容が表示されてしまう。とはいえこれってユーザにとってちょっと不便なだけでもあるので、これがどこまで大きな問題として扱うかはシステムやデータの特性次第とも言える。

実装の観点でいうと基本的にはLIMIT/OFFSET方式に加えてページ数の計算が増える。なお、総件数についてはどこで保持するかという問題がある。件数が多いと毎回計算なんてやってられない。別途それ用にRedisとか使ってもいいけどひとまず最小限の労力でやるならキーとなるIDごとの総件数だけを保持するKey-Valueなテーブルを用意して更新時にカウントアップするっていう方法もなくはない。

カーソル方式は次のページの先頭レコードを何らかのトークンやカーソルといったもので保持したり指定する方式。ここでのカーソルの値は対象オブジェクトをシリアライズしたものだったりなんでもいい。

この方式だと先に挙げた一貫性の問題は比較的大丈夫と言える。なぜならあくまでも指定したカーソル以降のデータを取得するので対象のデータ内にデータが追加された場合を除けば重複の問題はおきないだろう。時間が経った後にアクセスしても内容が変わることも無い。

だがカーソル方式だとUIでよくある何ページ目を表示するっていうオペレーションは実現できない。また特性上前のページに戻るというオペレーションを実現することも難しいだろう。一方で件数がめちゃめちゃ多いときとかにはいい気がしていて、無限スクロールとの相性はいいと思われる。

実装的にはこの中では一番手間がかかる。といっても大した手間ではないが。

基本的な流れは以下のような感じ。

  1. カーソルの値のデコード
  2. デコードした値を起点にクエリ実行。このときページサイズの+1件を取得する
  3. +1件で取得したレコードのID等をカーソルの値としてエンコード
  4. 3の値とともにレスポンス

2でやる+1件は次のカーソルを取得するためだけど、結果セットの最後のレコードで代用することもできると思う。

ちなみにこの書籍ではREST APIの実装パターンが数多く紹介されているのだが、ここではページネーションの方式としてカーソル方式を紹介している。

そして避けるべきパターンとして上で紹介したLIMITとOFFSETを用いるパターンを挙げている。

その理由としては『実装の詳細がAPIに表出してしまうこと』をあげていた。あとは将来的にデータストアが分散DBなどの複雑なものになった場合にオフセットの開始点をどう見つけるかといった点で計算コストなどの課題が出てくると。

そして先ほどの一貫性の問題も挙げられている。

レスポンス

  • Linkヘッダ
  • レスポンスボディ

ページネーションでは次のページの情報などのメタ情報が必要をクライアントに伝える必要があり、これをレスポンスのどこかに格納する必要がある。とはいえ、これはLinkヘッダかレスポンスボディの2パターンくらいだと思う。別のレスポンスヘッダに格納することもできるが。

Linkヘッダを使うパターンは本来レスポンスとして返す情報とメタ情報が分かれているので、個人的には綺麗だと思っている。

例えばGET /users?page=2のレスポンスに以下のようなヘッダをつけて返す。

Link: <https://example.com/users?page=1>; rel="previous",
         <https://example.com/users?page=3>; rel="next",
         <https://example.com/users?page=10>; rel="last",

一方で利用するフレームワークSDKにこのあたりの機能が提供されていないとサーバーサイド、クライアントサイドともに処理が多少面倒な可能性がある。サーバーサイドはヘッダ情報を組み立てる必要があるし、クライアントサイドはレスポンスからLinkヘッダを取り出し、パラメータを取り回す必要がある。

レスポンスボディに含めるパターンは深く考えなくていいので簡単と言える。1つのレスポンスボディ内に本来返すべき情報とは別にメタ情報を格納するプロパティを用意して一緒に返すだけだし、クライアントサイドは普通にレスポンスボディをparseすればいいだけだ。多くの場合はJSON形式だろうからどの言語でも簡単に目的の値を探索できる。

先の例と同じく例えばGET /users?page=2にリクエストするとレスポンスとしてこんなものを返す。

{
  "_link": {
    "previous": "https://example.com/users?page=1",
    "next": "https://example.com/users?page=3",
    "last": "https://example.com/users?page=10"
  },
  "users": [
    {
      "id": xxx,
      "name": xxx
    },
    {
      "id": xxx,
      "name": xxx
    }
  ]
}

なお、Linkヘッダの場合もレスポンスボディの場合もパラメータ名ないしはプロパティ名については各社様々だが、Linkヘッダそのものはこういったものだ。

Link - HTTP | MDN

各サービスの実装

次に著名な各Webサービスがこのあたりをどういった形で提供しているのか参考に見てみる。令和時代の新実装が見つかることを期待している。

GitHub

REST API 内での改ページ位置の自動修正の使用 - GitHub Docs

大正義GitHubAPIではどうなっているか。

GitHubでは呼び出すAPIによって返す値のデフォルト件数が異なるとのこと。ページネーションに関する情報は、API 呼び出しの Link ヘッダーで提供され、次のページと最後のページへのリンクがそれぞれ以下のようにLinkヘッダに含まれている。

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"

クライアントは次のページをリクエストするときはrel="next"となっているリンクをそのまま指定すればいいし、総ページ数はrel="last"のリンクからわかる。ただし、 URLを推測したり自分で構築するのはダメってことだが最初のリクエスト以降はジャンプするのもOKみたいだ。例えば14ページ目にジャンプしたときはLinkヘッダの中身は以下のようになる。

Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
  <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"

rel="next"rel="last"はそれぞれ先ほどと同じく次のページと最後のページ(総ページ数)だが、新たにrel="first"と'rel="prev"'というのが追加されている。これらはそれぞれ1ページ目と前のページってこと。ヘッダからの値の取り出しとかっていう処理はあるものの見た目にはとてもわかりやすい。

Twitter

Pagination | Docs | Twitter Developer Platform

Twitterの場合はレスポンスに含まれるnext_tokenprevious_tokenおよびリクエストのクエリパラメータのpagenation_tokenでコントロールする。初回のリクエストのレスポンスに含まれるnext_tokenの値をpagenation_tokenというクエリパラメータで指定してリクエストする。2ページ目以降のレスポンスにはprevious_tokenも含まれる。この値は単なるページ番号ではなく数桁の文字列となっている。

1ページあたりの件数はmax_resultsで指定できるようだが総件数はわからないっぽい。また、ダイレクトに特定ページにジャンプすることも難しいと思われる。

Method: cse.list  |  Custom Search JSON API  |  Google Developers

これはGoogleの検索結果をAPIで取得するのに使うもの。

さらっとドキュメント見ただけでは正直なところよくわからなかったのだが、どうやらクエリパラメータのnumstartでコントロールするみたいだ。numで1回のリクエストに含まれる件数を指定する。startは何件目から取得するかを指定するパラメータみたいで、例えば1回あたり10件だと2ページ目、つまり11件目以降を取得する際はnum=10&start=11と指定することになるらしい。

そしてレスポンスはさらに複雑だった。レスポンスのJSONにはrequest/previous/nextが含まれていてそれぞれにtotalResultsstartIndexといった値が含まれている。

Amazon Product Advertising API

Introduction · Product Advertising API 5.0

Amazonの商品検索とかのAPIであるがここではSearchItems APIを参考にしてみる。

SearchItems · Product Advertising API 5.0

シンプルにリクエストのパラメータとしてItemPageItemCountを指定する。ただしAmazonの場合はクエリパラメータではなくリクエストボディにJSON形式でセットする模様。そしてレスポンスにはTotalResultCountに総件数が含まれるのでそれを使って自分で計算してリクエストするみたい。

こういう形式って初回のリクエストをしてようやく総件数がわかるんだとすると検索結果下部に各ページへのリンクを張るようなUIは構築しにくいような気もするがどうなんだろうか。

Atlassianのconfluence

Pagination in the REST API

クエリパラメータのlimitで件数を指定し、startでオフセットを指定するパターン。そしてページの情報はレスポンスボディに含まれている。

    "_links": {
        "base": "http://localhost:8080/confluence",
        "context": "",
        "next": "/rest/api/space/ds/content/page?limit=5&start=10",
        "prev": "/rest/api/space/ds/content/page?limit=5&start=0",
        "self": "http://localhost:8080/confluence/rest/api/space/ds/content/page"
    },

格納しているURLとしてはLinkヘッダの場合と同様だけど、nextとかprevとかがレスポンスのJSONのプロパティとして表現されているので取り回しは楽そう。

Strapi

Sort & Pagination for REST API - Strapi Developer Docs

strapiも基本的にはページ番号指定方式でメタ情報はレスポンスに含める形式。ただし、クエリパラメータとして指定するページ番号の指定の仕方はちょっと独特だった。

GET /api/articles?pagination[page]=1&pagination[pageSize]=10

レスポンスに含まれるメタ情報はトータルの件数にページサイズ、現在のページ、そして総ページ数。

Stripe

Stripe API reference – Pagination – curl

Stripeはカーソル方式。1回のリクエストで取得するオブジェクトの数をlimitというクエリパラメータで指定する。そして取得したオブジェクトの最後のオブジェクトのIDをstarting_afterというクエリパラメータで指定するとそのオブジェクト以降のオブジェクトをリストで取得できる。

こういう方式なのでレスポンスには取得したオブジェクト以降にもデータがあるかを示すhas_moreというフラグ以外に特にメタデータは含まれていない。

WordPress

Pagination | REST API Handbook | WordPress Developer Resources

WordPressはページ番号指定方式。だけどOFFSETっぽいこともできる。pageというパラメータで直接指定することもできるし、そのスタート位置をoffsetというパラメータで指定することもできる。例えばページサイズがデフォルトの10件の状態でoffset=5と指定した場合に最初のページは6件目から15件目までのものがレスポンスされる。

そしてメタ情報はレスポンスのヘッダに含まれるのだけどLinkヘッダではなくX-WP-TotalX-WP-TotalPagesという2つのヘッダを用いる。次のページに関する情報は特段レスポンスしない模様。

Django

最後にこれはサービスではないんだけどPythonフレームワークであるDjangoがページネーションをサポートしていてドキュメントにもまとまっている。

Pagination - Django REST framework

Djangoの場合、紹介したパターンすべてをサポートしている。つまりリクエスト方式としてはLIMIT/OFFSET方式、ページ番号方式、カーソル方式のいずれもサポートしているし、メタ情報の返し方としてもボディに入れる方式とLinkヘッダに入れる方式のどちらもサポートしているのだ。加えてContent-Rangeというヘッダでも返せるっぽいのだけど詳細は見つけられなかった。

では我々はどうするか

ここまで思いつくパターンと有名企業各社やプロダクトの実装を眺めてきた。紹介はしていないが実は他にもいくつか見たのだが傾向としては以下のような感じだ。

  • リクエストはページ番号指定方式が多い
  • メタ情報はレスポンスボディに含める
  • 特に目新しいパターンはなかった

というわけで検討した結果、今回は以下にすることにした。なお、今回はキャッシュのしやすさや実装におけるパフォーマンス面での比較検討はできていない。

  • リクエストはページ番号指定方式
  • メタ情報はLinkヘッダに含める

まず、リクエストをページ番号指定方式にした理由はシンプルに今回作るシステムのUIで特定のページにジャンプさせるようなものを用意するからだ。

こういうやつ。

また、LIMIT/OFFSET方式はDBの実装が露出しているようで嫌だしカーソル方式だとこのようなUIは難しいのではないかと思った。また、一貫性の問題については許容することとした。

メタ情報をLinkヘッダに格納するかレスポンスボディに入れるかはどちらでもいいと思う。が、今回はLinkヘッダに入れる方式を採用した。理由としてはそれほど深いものがないのが正直なところだけど、一応思ったのはレスポンスボディには本来欲しい情報の結果だけにしたいなというのが大きい。

あとはLinkヘッダ方式の悩みどころとして値の取り回しだったのでクライアント目線だとレスポンスボディに入れたほうがいいかなと思っていたんだけど、これについてはこんなライブラリ見つけたのでそこについてもまあいっかなと。

www.npmjs.com

最終的には両方サポートするかもしれない。そんなに手間でもないので。

まとめ

  • 令和時代の〜という大げさなタイトルにしたものの、特に新しい方式やトレンドがあるわけではない
  • とはいえLinkヘッダ方式は10年以上前にはあまり見かけなかった気もする
  • 自分の実装はLinkヘッダとページ番号指定方式にした
  • GeoJsonも扱う必要があるがこれについてはどうすればいいかさっぱりわからなかった

というわけで『令和時代のページネーションを考える・実装編』に続きます。実装し始めたら考えが変わるかもしれない。

Kindle本読書の秋キャンペーンで安くなっているおすすめIT本19冊

いつもAmazonで見つけて気になった本とか、自分がよかった本、セールになってる本をちょこちょことTwitterとかで紹介してるんだけど、2022年9月30日(金) 00時00分 ~2022年10月13日(木) 23時59分の期間で開催されている『【最大50%OFF】Kindle本読書の秋キャンペーン』のコンピュータ・ITカテゴリの書籍が定番本が多かったのでまとめて紹介します。 ちなみにいずれも自分が過去に実際に買って読んだ本ばかりだけど、正直なところ昔読んだやつは細かい内容を覚えていないものもある。

キャンペーン自体は技術書以外もいっぱい対象になっています。むしろ自分は技術書以外のおすすめを教えてほしい。

amzn.to

定番になってる本はこういう機会にとりあえず買っておくと読みたくなったときに良い。それ以外にも自分は今すぐには必要ないけど近い将来役に立ちそうだなと思えるようなものはこういう機会にとりあえず買っておいて必要なときにストレージの底から引っ張り出してくるってこともままあります。

なお、独断と偏見によるものなので賛否あるとは思うけど気にしない。

価格については2022年10月5日21時43分時点のものです。

プロダクトマネジメントのすべて 事業戦略・IT開発・UXデザイン・マーケティングからチーム・組織運営まで

ポストモーテム みずほ銀行システム障害 事後検証報告

  • 990円
  • 界隈では非常に話題になった本
  • 個人的には第三者が書いていることで、社会への影響が大きい問題でもあったのでジャーナリズム的にはいいのかもしれないけどなんとなく余計なお世話感もしてしまう
  • とはいえ何が起きていたかを知れる一冊ではあるのでこういう機会に入手して読んでみるのもいい
  • 自分は同僚が読んだって話を聞いて購入した

達人に学ぶDB設計 徹底指南書

  • 1430円
  • 久しぶりにRDBMSの論理設計について復習しようと思って購入した
  • 2013年発刊と少し古いものの、RDBにおけるテーブル設計の原理原則については変わることあまりないので十分
  • 網羅的にかかれているので一冊持っているといいと思う

やってはいけないデザイン

  • 990円
  • プロダクトのUXとかUIについて考えることが多いものの自分には致命的にセンスがないので少し勉強してみようと思って購入したもの
  • この本自体は自分の主戦場であるWebとかモバイルとかそういうのをターゲットにしたものではない
  • とりあえず余白大事ということがわかった

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本

  • 1760円
  • 最近また盛り上がりを見せつつあるDDDに関する本
  • 表題通り入門書として概観を掴むのにとてもいいと思った

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ

  • 1375円
  • 先に紹介した本と同じ著者によるもので大ロングセラー本といえるのではないか
  • こちらはDB設計ではなくSQLの書き方、テクニックについて網羅的に書かれている
  • こちらも発刊は古いが現代でも十分役に立つ

エリック・エヴァンスのドメイン駆動設計

  • 2860円
  • ドメイン駆動設計に関する大定番の書籍
  • DDDやるやらないに関わらずこの業界にいるならば一読して頭の片隅には入れておくといいと思う

暗号技術のすべて

  • 1925円
  • 暗号技術についてかなり詳細に書かれている
  • その分、とてもハードボイルドな一冊に仕上がっているので挫折しそうになる

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習

  • 2090円
  • これも同作者によるもの
  • セキュリティを学ぶには攻撃手法について学ぶのが良い、そしてそれには実際に手を動かすのがいいってことで自由に攻撃の実験ができる環境を物理ではなく仮想環境で用意していきましょうという本
  • 少し古い本なので使うツールについては今っぽくないものもあるけど

実践ドメイン駆動設計

  • 2860円
  • こちらもDDDに関する本
  • 決して僕がDDDに傾倒しているってわけではないのだけれどソフトウェアエンジニアなら知っておいて損はないと思ってて、定番と呼ばれるものが安くなってるからご紹介

アルゴリズム図鑑 絵で見てわかる26のアルゴリズム

  • 1309円
  • 基本的なアルゴリズムについて図示しながら説明してくれている本
  • データ構造、ソート、配列といったデータ操作に関する基本的なものからセキュリティとか暗号といったものまでをカバー

カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで

  • 1265円
  • アジャイルに関してストーリー仕立てで学べる
  • 著者は一部の方にとても高い人気なんだけど、彼の選ぶワードは独特の熱さがあると思っててそれがいろんな人に響いてるんだろうな
  • システム開発に直接的に関わる本ではないけど

チーム・ジャーニー 逆境を越える、変化に強いチームをつくりあげるまで

  • 1320円
  • 一つ前の書籍と同じ著者によるもの
  • 『チーム』にフォーカスした本なのでリーダーとかの人におすすめ

知識ゼロから学ぶソフトウェアテスト 【改訂版】

  • 1320円
  • これもロングセラー本ですね、その改訂版

詳解UNIXプログラミング 第3版

  • 4565円
  • これもメジャーな本ですがお高いのでこういうときに買っておくのがいいと思うというわけでご紹介
  • この本は本当にすごい、これでもかってくらいUNIXについて書かれている
  • その昔、読んだときは正直わからないことも多かったけどリファレンス的にでも持っておくのもいい
  • この分厚い本が電子書籍になっていつでも持ち運べるようになって技術の進化素晴らしい

OAuth徹底入門 セキュアな認可システムを適用するための原則と実践

  • 2310円
  • OAuthに関して網羅的に書かれている本
  • どちらかというと利用者側、つまりクライアント側の視点からだけではなく実装側についても解説されている
  • 実装に関するサンプルコードも多数
  • ただし読みやすくはないと思う

Amazon Web Servicesクラウドデザインパターン設計ガイド 改訂版

  • 1485円
  • 往年のベストセラー本
  • AWSの本ではあるけどクラウドでシステム作る基本的な考え方は今でも有用だと思う
  • クラウドサービスのコンポーネントの組み合わせをデザインパターンとしてまとめあげられてる
  • 最新のマネージドサービスを使うことで解決してる問題もあるけどそれでも基本的な考えを知るにはいいと思う

エンタープライズアプリケーションアーキテクチャパターン

  • 3190円
  • かのマーティンファウラー氏によるエンタープライズアプリケーションに関する開発パターンについて記された本

テスト駆動Python

  • 1540円
  • 内容的にはpytestを使ったテストの実装方法について網羅的に書かれているので復習にも良い
  • これからpytest使ったテストをやっていきたいって人にもいいと思う
  • pytestだけでなく関連ツールとの組み合わせについてもさらっと解説されている
  • ただし、タイトルから誤解しがちだが『テスト駆動開発』については学べない
  • 実は第二版が出ているようなんだけど、残念ながら安くなってるのはこの第一版のほう

既存のデータベースでPrisma migrateする。しかもPostGISを使う

既存のデータベースをPrismaマイグレーションできるようにしたくなった。理由はいろいろあるがやはりローカル環境 → 開発環境 → ステージング環境 → 本番環境へとDBの定義を反映していくのが手作業はさすがにないなと思えてきたからだ。もちろん実際には毎回SQLを直接手で入力なんてことはないだろうけど。

あとは他の人が開発するのにDBのセットアップをする際にも楽だ。

というわけで導入しようとしたのだがそれなりに悩んだりしたのでメモを残しておく。

前提

  • すでに存在しているDBをPrismaのmigrationツール管理下におく
  • これまでは直接DBにDDL文を実行して定義などしていた
  • DBの定義はprisma db pullを実行してsyncしていた

やっていく

まずは既存のDBの定義とPrismaのモデルを同期します。普段からやってるものの念の為。

prisma db pull

同期したら早速マイグレーションのコマンドを実行します。以下ではDBのURLをマスクしてます。

prisma migrate dev --name initial-migration --create-only

✔ We need to reset the PostgreSQL database "postgres" at "xxxxxxxxxx.cluster-xxxxxxxxxxx.us-east-1.rds.amazonaws.com:5432".
Do you want to continue? All data will be lost. … yes

--nameはこのマイグレーションをわかりやすく識別するためにつける任意の名前。--create-onlyというのはマイグレーションファイルの作成だけで実際の適用はしないというオプション。

さて、こんな感じで実行すると不穏なメッセージが表示されている。Yesを選択して進めるとメッセージの通り、この時点で既存のDBのテーブルとデータは全て消えてしまう。--create-onlyにしているにも関わらず、だ。

Prisma migrationを導入しようとすると、最初のタイミングでmigration用の管理テーブルが作られる。そして手動で定義していたものについてはその時点で既存のテーブルは一度削除されてしまうという仕様のようだ。テーブルが削除されるのでもちろんデータも消える。開発環境とは言えそれなりの量のデータが入っていること、本番適用時にデータが全部消えてしまうのは論外なこともあり色々と調べたり試したりしたものの良い解決策は見つからなかった。そう、消えるのだ。--create-onlyを指定したところで消えるのだ。この場合の--create-onlyは確かに適用まではされない。

--create-onlyをつけて実行すると実行日時のフォルダとともにマイグレーションファイルができあがり、マイグレーションの管理テーブルだけが作成された状態でマイグレーションファイルの内容は反映されていない状態となる。大事なことなので2回言うが、DB上は全部のテーブルが削除された上でマイグレーションの管理テーブルのみが存在しているという状態になる。

これに関してはどうしようもないので既存のものに導入する場合、かつデータも残したい場合は事前にバックアップするなり、テーブル単位でエクスポートしておくことは必須といえる。

PostGISをどう扱うか

さて、自分のようにPostGISを使っている場合はそのまま実行すると失敗する。なぜならば、先の処理で一度DBの中身がまっさらになっていてPostGISのセットアップについてもリセットされた状態になるからだ。

これは作成したマイグレーションファイルの先頭に以下を追加することで対応できる。これは初回のマイグレーションファイルだけでいい。なお、このようにマイグレーションファイルを直接的に修正していいのは適用前のものに対してだけですでに適用したマイグレーションファイルの修正は禁物だ。

CREATE EXTENSION postgis;
CREATE EXTENSION fuzzystrmatch;
CREATE EXTENSION postgis_tiger_geocoder;
CREATE EXTENSION postgis_topology;
ALTER SCHEMA tiger OWNER TO rds_superuser;
ALTER SCHEMA tiger_data OWNER TO rds_superuser; 
ALTER SCHEMA topology OWNER TO rds_superuser;
CREATE FUNCTION exec(text) returns text language plpgsql volatile AS $f$ BEGIN EXECUTE $1; RETURN $1; END; $f$;
SELECT exec('ALTER TABLE ' || quote_ident(s.nspname) || '.' || quote_ident(s.relname) || ' OWNER TO rds_superuser;')
  FROM (
    SELECT nspname, relname
    FROM pg_class c JOIN pg_namespace n ON (c.relnamespace = n.oid) 
    WHERE nspname in ('tiger','topology') AND
    relkind IN ('r','S','v') ORDER BY relkind = 'S')
s;

そしてPostGISのgeometry型をPrismaはサポートしていない。したがってここについても修正しておく必要がある。実際には以下のように単なるgeometry型として生成されている。

CREATE TABLE "sample" (
    "id" SERIAL NOT NULL,
    "name" TEXT,
    "coordinates" geometry,

    CONSTRAINT "sample_pkey" PRIMARY KEY ("id")
);

これを以下のようにGeometryのタイプも指定する形に修正しておく。ここではPOINTを指定すると同時に座標系も4326を指定している。

CREATE TABLE "sample" (
    "id" SERIAL NOT NULL,
    "name" TEXT,
    "coordinates" GEOMETRY(POINT,4326),

    CONSTRAINT "sample_pkey" PRIMARY KEY ("id")
);

追加・修正したらマイグレーションを実行する。

データを戻す

今回のように--create-onlyで実行したあとに適用する場合はprisma migrate devを実行するだけでいい。なお、コマンドを見てわかるようにこの手順は開発環境向けであって本番適用手順はまた別だ。

prisma migrate devを実行したら必要に応じて先程のバックアップからデータを戻す。なぜならば前述のとおり一度テーブルが削除されているためデータが空っぽになっているからだ。自分の場合はpg_dumpを使ってテーブル単位でデータだけバックアップをとっておいてそれをインポートする方式で対応した。

2回目以降

2回目以降も同様にprisma migrate dev --name <名前>を実行していく。だが、個人的には毎回--create-onlyをつけてマイグレーションファイルの中身を目視で確認するのをおすすめしたい。

そして、2回目以降も実行ごとに日時のフォルダとマイグレーションファイルが作成される。

その他

さて、このようにPrismaによるMigrationを導入したあとはDBの定義に関して基本的にはschema.prismaで定義を行ってはマイグレーションを実行するという流れになる。仮にDB側で直接テーブルを作ったりするとprisma migrate resetが必要になってしまい、またテーブルがまるっと削除されてしまうので要注意だ。

また、前述のPostGISに限らず生成されたSQLが意図したものではない場合や望ましくない場合があるだろう。そんなときもマイグレーションファイルを修正して実行すればいいのでやはり--create-onlyをつけて確認するというのをおすすめしたい。

例えば列名の変更は通常は以下のようなSQLで実行できる。

ALTER TABLE テーブル RENAME COLUMN 旧列名 TO 新列名

だが、これをPrismaでやろうとしてschema.prismaのモデル上で変更すると以下のようなSQLが生成されてしまう。

ALTER TABLE テーブル名 DROP COLUMN 旧列名,
ADD COLUMN     新列名 TEXT;

そう、この場合だと旧列名の列を削除して新しい列名の列を追加するというSQLが発行されてしまう。つまりデータも消える。これはよくない。

そんなときにマイグレーションファイルの中身を修正して実行してあげれば問題ない。

ついでにPostGISを使っている場合にGeometry型の列に対してgistインデックスを張っている場合もあると思う。これに関してもPrismaでは対応していないのでまずは普通のインデックスを張るようにschema.prismaで設定した上で--create-onlyを実行する。作成されたマイグレーションファイルでは当然ながら普通のbtreeなインデックスが作成されるようになっているのでこれをgistインデックスに手動で書き換える。

具体的には、列指定の前にUSING gistを追加してあげるだけでいい。以下のように。

CREATE INDEX "idx_geo" ON "sample" USING gist("geo");

まとめ

というわけでPostGISを使っている場合は少し手を入れる必要はあるが、基本的には問題なくPrismaによるマイグレーションができる。そしてPrismaを信じきらず必ず--create-onlyで適用前に確認することをおすすめする

一意な識別子の生成でUUID/ULID/CUID/Nano IDなど検討してみた

最近、一意な識別子について検討することがあったのでその検討メモ。

一意な識別子とは

つまり、重複しない、ユニークな識別子(Identifier, 以下id)のこと。ここではRDBのテーブルにおける主キーとして使うことを想定かつ前提としている。したがって、主キーの要件であるユニーク性を持ったidをどうやって生成していくか。

そんなのDBの連番でいいじゃんて話もあるがここではその話はせず、あくまでも一意な識別子をどう生成するかの話に絞る。

選択肢

一番有名だと思われるUUIDを筆頭にいくつかの選択肢がある。

  • UUID
  • ULID
  • CUID
  • Nano ID

他にもTwitter発のSnowflakeとか今はDeprecatedになってるshortidなどがあるが、キリがないのでここでは上記の4種類だけで簡単に比較した。また、実際にはUUIDはバージョンによってSpecが異なるがここではバージョン4を前提として話をすすめる。また、本来はパフォーマンス比較もしたほうがいいが今回はやれていない。

UUIDの例は 712a865f-79e1-4976-91f1-5150b0e1b9c0 といった形式。UUIDはバージョン4だとランダムに生成されるので耐衝突性が高い、つまりユニーク性は高いとされている。一方で連続性が低く時系列的なソートが効かないなどある。

import { v4 as uuidv4 } from 'uuid';
console.log(uuidv4());

それを解決したといえるものがULIDだ。ULIDはユニーク性を維持しつつミリ秒単位でソートが可能というもので、サンプルとしては01G9BRJVBB3GTYDRT01FBPY24Vという感じだ。ランダムに生成される部分がUUIDは122ビットに対してULIDは80ビットなのでランダム部分のユニーク性はUUIDよりは劣るがタイムスタンプ部分との組み合わせで実質的に問題なさそう。

import { ulid } from 'ulid';
console.log(ulid());

次のCUIDはUUIDの問題を解決しつつULIDよりもいいとされているもの。水平方向のスケーリングとバイナリサーチにおける検索性能の高さを目的としているそうで、cl6a7y23f00006ipga93g41x0といった形式。UILDとの比較はこちらにあるが端的に言うと、

  • CUIDはCSRNGに依存していない
  • これまで衝突が報告されたことはないし、多くのパッケージやリポジトリで使われている

だそうだ。多くのパッケージで使われているとはいえUUIDには遠く及ばない。だが個人的には十分な数ではないかと思う。

import cuid = require('cuid');
console.log(cuid());

Nano IDは端的に言うとUUIDをより小さく・高速にしたものと言える。実際の例はFijSqjkogl2JzJ16P1trhといった形式で確かに文字数としても少ないことがわかる。

import { nanoid } from 'nanoid';
console.log(nanoid());

最後に、UUIDはRFC4122 (https://datatracker.ietf.org/doc/html/rfc4122)として標準化されているのは人によってはポイント高いかも?

ダウンロード数

npmのダウンロード数も比較してみた。

うん、比較が意味をなさないくらいに圧倒的にUUID。そしてNano ID。ULIDとCUIDについてはこの2強の前には大した違いがないレベル。

というわけでULIDとCUIDだけで比較。

CUIDのほうが倍くらい使われてるっぽい。これはちょっと意外。Googleで検索すると体感ではULIDのページのほうがヒットした印象だったので。

注意

前述の通り、上記の比較はあくまでも簡単な比較であって生成にかかるパフォーマンスの比較やベンチマークはしていない。これはどこかで実際に自分でやってみたいところ。

まとめ

結論として何を使うかって話だけど基本はUUID、個人的にはUUIDの長ったらしいフォーマットが好みではないのでNano IDでいいかなと思っているがこれは利用するアプリケーションなどにもよりそう。

また、時系列によるソートが必要ならULIDかCUIDだがここは好みだけど実績の多そうなCUIDか。

なお、自分が使っているPrismaではUUID (v4のみ)とCUIDしかサポートされていないこの場合、時系列性のないUUIDのv4だといろんな方面で議論されているようにMySQLではインサート時のパフォーマンスが悪い。なのでPrismaとの組み合わせで使う場合は基本的にCUIDかなと考えている。自分の場合、現在使っているRDBPostgreSQLなのであまり関係ないけど。

Ubuntuのコンテナにwatchmanを頑張って入れる

メモです。

元はServerless Frameworkでserverless-appsync-simulatorとserverless-offlineを使おうと思ったら Error: spawn watchman ENOENTっていうエラーが出て起動できず、ぐぐったらwatchmanをインストールすればいいってことでその対応。

MacならHomebrewであっさりインストールできるのだが、あいにく自分はVS Code + Remote Containersである。コンテナとしては公式のNode.jsのイメージを使っていてこれはUbuntuベース。

というわけでLinuxにwatchmanをインストールしようとしたらそこそこハマったのでそのメモ。

まず、Linuxにwatchmanを入れるにはソースからコンパイルして入れる必要がある。一応バイナリも配布されているらしいが。

ソースからコンパイルしてインストールするにあたり事前に以下のパッケージをインストールしておく必要がある。

apt install -y make libtool libssl-dev autoconf automake pkg-config g++ libpython-dev

最後のlibpython-devってのは今回自分が使っているようにPythonがインストールされていないイメージではpyconfig.hが存在せずビルドに失敗するから。

これらを入れたら以下のようにGitからソース持ってきて入れる。

git clone https://github.com/facebook/watchman.git -b v2022.07.18.00 --depth 1
cd watchman
./autogen.sh

なお、公式のインストール手順だとGitからクローンするバージョンがv2021.09.20.00と古い。そうするとpcreのダウンロードに失敗するので新しいものを使う。そして公式手順では./autogen.shの次に./configureしてmake installするというおなじみの手順があるのだが、新しいものを使うと./configureが生成されずmake installもできない。というわけで以下のようにやっていく。

まず、そのままだと./autogen.shにも失敗する。どうやら新しいやつだとRustも必要だったのでインストールする。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

Rustをインストールした後に./autogen.shを実行する。 前述の通り、masterの最新でビルドした場合はconfigureが生成されず、make installもできない。そのためこのあたりを手動でセットアップする必要がある。まず、ビルドされた成果物はbuild/binフォルダにあるのでこのディレクトリに対してPATHを通すか、すでにPATHが通っているどこかのディレクトリに中身をコピーする。自分の場合はひとまずすでにPATHが通っている/usr/local/binにコピーした。

そしてwatchmanを実行してみるとこんなエラーが出る。

./built/bin/watchman: error while loading shared libraries: /usr/local/lib/libgflags.so.2.2: cannot open shared object file: No such file or directory

これはビルドされた共有ライブラリをちゃんと参照できていないということ。これらはbuild/lib以下にあるのでこれを/usr/local/libにコピーした上で環境変数LD_LIBRARY_PATHをセットした。

cp -p ./built/lib/lib*
export LD_LIBRARY_PATH=/usr/local/lib/

そして実行してみると今度は次のエラーである。

[watchman] while computing sockname: failed to create /usr/local/var/run/watchman/root-state: No such file or directory

これは単にディレクトリがないだけなので作成する。

mkdir -p /usr/local/var/run/watchman

ここまでやってようやく起動に成功する。ちなみにautogen.shの実行は結構時間がかかる。コンテナのなかで実行してるからかもしれないが。自分の環境だと10分以上は平気でかかった。

本来の目的であったsls offline startも問題なく実行できた。めでたし。

あとはこれをDockerfileに起こすだけ。

我が家のYouTube撮影環境を晒す

はじめに

Twitter等で僕のことを知ってる人はご存知の人も多いのですが、実は今年の4月から小3の娘とYouTubeのチャンネルをやってます。

本当ならチャンネル名とかをここで公開して宣伝したいところなんですが、いろんな考えから完全に名前とか顔とかも含めて僕とのつながりも一切出さない感じでやってます。 動画のジャンルとしてはゲーム実況が中心です。たまに娘自体の日常的な内容もあげてます。

さて、演者としては娘がメインでやってるわけですが実際のところ動画の撮影や編集とかは僕がやってるんですね。そしてゲーム実況、かつVTubeってことで全くそっち方面の知見がなかったのですがなんとか固まってきたので同じような人の参考になるかもということで公開しておきます。きっと今後もアップデートされていくとは思うんですがまずは今時点の環境ということで。

メイン動画

メイン動画の場合は2パターンあります。ゲーム実況を撮る場合とリアルなものを撮る場合です。リアルなものってのは例えばポケカ開封動画とかそういうやつですね。

ゲーム実況の撮影環境

ゲーム実況の場合は当然のことながらプレイ中の映像を何らかの方法で撮影して取り込む必要があります。また、顔出しをしていないので人を撮る必要はないのですが、VTuberとしてやってるのでモデルを動かすためのモーションキャプチャ用のカメラが必要になります。

構成

こんな感じになっています。

キャプチャボード: Elgato HD60 S+

まず、HDMIキャプチャとしてはTwitterで教えてもらったElgatoのものを使っています。

使っているPCがM1 Macなのでちゃんと動作するか不安だったのですが、こちらでちゃんと動作しています。ここにSwitchのドックからHDMIで出してHD60のINにつないでいます。

そして、HD60のアウトからテレビに出しています。そうすることでSwitchの映像をテレビに出しつつ画面を録画できます。

あとはHD60をMacとUSBで接続するだけです。ちょっと面倒なのは動画撮影をしていないときに普通にSwitchをテレビに映して遊びたいときに、つなぎ直しをするか、HD60に電源供給するかしないとダメなことですね。

マイク: Blue Yeti Nano

次に音声ですが、Yeti Nanoを使っています。

これは家にある指向性マイクがこれだけだったからという理由で使ってるのですが、二人でやるときとかに少し音が遠くなるときがあるので近いうちになんとかしたいと思っています。

ただし、単品で見ると音はとてもいいと思います。

あと、実際にはKrispというソフトウェアノイズキャンセリングを使って収録しています。これは家で取るときにいろんな音が入ってしまうからなのと、あとから編集でそれらを取り除くのが面倒だからです。弊害として最初の挨拶とかの音の立ち上がりがやや遅れるというのがあるのですが目をつぶっています。

アバター: VTube Studio

VTube用のモーションキャプチャMacの付属のカメラを使っていまして、VTube Studioを使ってLive2Dのモデルを動かしています。

denchisoft.com

このVTube Studioを使って動かすLive2Dのモデル自体はnizimaで娘と選んで購入しました。

nizima.com

VTube StudioはそもそもMac版はExperimentalな感じなのと、さらに何かといろいろ動かないM1 Macで動くのかっていうのがあってドキドキしていたのですが、一応基本的には動いています。

OBS

さて、これらの入力をもとに実際に録画をしているのはOBSというソフトウェアです。これはライブ配信とかでも使われる大定番ですね。僕がこれまでやっていた勉強会のライブ配信でも使っていました。

obsproject.com

まず、ゲームの映像はOBSで『ビデオキャプチャバイス』としてHDS60を選択して取り込んでいます。

次に、VTuberスタイルの動画を作るためにVTube Studioの映像をOBSに取り込むところですが、これがネットによくある記事とちょっと構成が違っています。ネットにある記事はWindowsを前提にしていることが多く、ゲームキャプチャを使って設定する方法がよく解説されていますがMac版ではこれが使えません。

というわけで普通にウィンドウキャプチャを使って取り込んでいます。そしてアバターだけをOBSに映すためにVTube Studio側で背景を緑にします。ここでWindwosでは『透明化』が選択できるようなのですがMac版では無理です。そこでウィンドウキャプチャのフィルターとして『クロマキー』を使用しています。そうすることで緑色の背景が透過してアバターだけがOBSに表示され、任意の背景を設定したりゲーム画面にかぶせたりできます。

本当は参考画像のスクリーンショットとかを載せたいところなんですが、冒頭でお話したとおりこのYouTubeチャンネルと僕の関係は完全に伏せているので載せられません。なのでこのあたりで質問があったらTwitterのDMでどうぞ。

ここまでやったらあとはOBSの録画ボタンを押すだけです。

ちなみにこれらを1台のMacでやっているのですが使っているのはMacBook Pro (14-inch, 2021)でCPUはM1 Maxのメモリ64GBモデルです。

リアル動画の撮影環境

こっちは普通の動画撮影と同じ感じですね。一応紹介しておくと、

Sony α6400

コスパ抜群のソニー製ミラーレスですね。ほとんどこれ一本で撮ってます。

Sony SEL20F18G

これもコスパがいいソニーレンズです。いわゆる明るい単焦点。広角が好みなのでこれを使うことが多いですがポケカの動画とかだと逆に余計なものが映り込みすぎるので最近はあまり使っていないです。なお焦点距離が20mmのレンズですが、α6400はAPS-Cなので実際には1.5倍くらいになります。

Sigma 56mm F1.4 DC DN

こちらもコスパのいいSigmaのレンズですね。こちらも明るい単焦点なんですが56mmと望遠気味な上にAPS-Cのα6400と組み合わせると結構寄った感じの画角になります。またF値が先のSEL20F18Gよりも小さい1.4となっているのでより明るいです。屋内での撮影に向いていると思います。しかも安い。

K&F Concept SA254T

これは三脚です。三脚なんですがいろんな使い方ができる上にコンパクトに収納できる三脚です。YouTubeの撮影でこの三脚を使う最大のポイントは俯瞰撮影ができるということです。ポケカ開封動画とかはこれで撮っています。手元を上から見下ろす感じで撮影するあれですね。

俯瞰動画をするための機材はいろいろあるんですが、K&F Conceptのこれ一つあれば俯瞰動画にも対応できた上で最大で全高238cmまでいけるので娘の運動会の撮影にも使えます。しかも一脚にも出来たり、逆マウントもできたりといろいろできるのでYouTube以外でも使い道が多いです。そして安い。

動画編集

動画の編集はAdobeのPremiere Proを使っています。利用経験があったとかではないんですが、AdobeのCCを契約していてそれに含まれているからってだけの理由です。

一番大変なテロップ入れについてはVrewとか使う人も多いかと思いますが僕はPremiere Proの機能である『自動文字起こし』を使っています。AI()が自動で文字起こしをしてくれるっていうあれです。Vrewよりも使い勝手がいいように感じたのでこちらを使っています。

ただ、確かにまるっとやってくれるのはいいんですが、それでも細かい修正とかテロップごとにスタイルを適用したりってところがまだいい感じでフロー化できていなくて四苦八苦するのでテロップなしで出してしまうこともしばしばあります。

音楽とかは自分のライブ配信Podcastでもお世話になってるArtlistを使っています。

https://artlist.io/

ショート動画

ショート動画も結構公開しています。メインのゲーム実況とは別の日常的なやつとかちょっとしたネタ的な動画です。あとは宣伝目的の本編を切り取ったものですね。

こっちはゲーム実況とかするわけでもないので難しいことはしていません。

撮影機材

動画を撮る場合は手持ちのiPhone 11 Proで撮ってます。ちょっと古めなんですがまあ十分かと。どちらかというと機動力重視で撮ってすぐに編集してそのまま出すって感じでやってます。

ショート動画ってアプリからしか投稿できないと思うので、普通のカメラとかで撮っちゃうとデータの移動とかが大げさになって面倒なのでこうしてます。

あと、ショート動画は実際には画像をつなぎ合わせて動画を作ることも多いです。

動画編集

https://www.capcut.net/?language=ja

ショート動画の編集はCapCutを使ってやってます。これはiPhoneアプリですね。以前はAdobeのPremiere Rushというアプリを使ってやってたのですが目隠し用のスタンプを自動で追従させるのがCapCutのほうが楽なので今は全部CapCutで作ってます。文字入れとかの使い勝手もCapCutのほうが楽だと思います。

前述のとおりiPhoneで撮ってそのままCapCutで編集して、文字入れなんかをしつつ書き出してYouTubeのアプリからアップという感じでやっています。すべてiPhoneで完結させる感じですね。

メイン動画の宣伝目的で切り取る場合もメイン動画の元データをDropboxに保存してるのでそこから読み込んで、CapCutで編集してという感じで同じ流れです。各種画像から動画を作る場合も同様です。

まとめ

というわけで意外と簡単にできるということがわかったと思います。そして一番大変なのは今回取り上げた環境周りではなく編集です。単純に時間もかかるし、センスも必要なので両方ない自分にとってはまさに地獄。

あとは撮れるときにストックをいっぱい撮っておくことが重要ということを理解しました。

収益化とかはまだまだ遠いですが楽しんでやっていきます。

©Keisuke Nishitani, 2023   プライバシーポリシー