見出し画像

仮想政府ニャルニアの歩み:3日目・データベースの基礎設計

ニャンマルハウト「MySQLが突然MariaDBになった時、俺は宇宙猫になった」

執務室にて

ニャオネラ「ではニャンマルハウト、引き続き公共事業プロジェクトを推進してください。ニャルニアの未来はあなたの額に乗っていますよ」

ニャンマルハウト「はッ!」

ニャンマルハウト「…………」

ニャンマルハウト「ふう。女王様を前にすると、一端の雄猫である俺でも緊張するな」

ししゃも丸「迫力がすごいし、きれいなひげだった

ニャンマルハウト「何だお前、ひげ派か。俺は前足の爪派だ」

ししゃも丸「それより、なんで女王様が来てたんだ?」

ニャンマルハウト「公共サービスAPIの実現は女王様のマニフェストなんだよ。上手く実現できないと公約不履行で妹のニャニャシ様に王座を乗っ取られる

ししゃも丸「そりゃあ必死にもなるなぁ」

そもそもデータベースって?

ニャンマルハウト「さて、前回でAPIの基本的な仕様は完成した。もちろんまだまだ一部だが、まずは今設計が終わっているところまで作っていこうと思う」

ししゃも丸「ようやくプログラムを作るのか?」

ニャンマルハウト「残念ながら、まだだ。次は、データベースの設計になる。まずその下準備として、プロジェクトのディレクトリ構造を整理しておこう」

.
├── db
└── schema

現在のディレクトリ構造。

ニャンマルハウト「dbにはデータベースの設計を入れる。要するに、dbディレクトリの内容を使って、データベースを初期化したり、更新したりするわけだ」

ししゃも丸「なるほど。schemaは?」

ニャンマルハウト「ここには、昨日つくったAPIスキーマを入れている。APIスキーマというのは、つまりAPIの仕様ということだな」

ししゃも丸「あの115行もあるやつか」

ししゃも丸「……というかさ、ニャンマルハウト。そもそもデータベースってなんだよ。俺はデータベースってのがなんなのか、よく知らないぜ」

ニャンマルハウト「一言で説明するのは難しいな……。たくさんのデータをプログラムで扱う時は、いくつか一般的な課題がある。1つ目はデータを正確に取り扱えること……これが出来ないと、データが壊れてしまう不具合が起こることになる。そして2つ目はデータを高速に取り扱えること……これが出来ないと、現実的には使い物にならないシステムになってしまう。この両方をうまく解決してくれるソフトウェアが、データベースだ」

ししゃも丸「なるほど。めっちゃ頭いい猫ってことだな」

ニャンマルハウト「猫ではないが、まあ図書館司書猫でもイメージするといいだろう。今回利用するのはリレーショナルデータベースという種類のデータベースで、一つ一つのデータを表で管理する。エクセルを使ったことがある猫は、アレをイメージするといいかもしれないな」

ニャンマルハウト「データベースを使うためには、最初に表の構造を決める必要がある。例えば、catsという表にはnameという列がある、ここには文字列が入る、みたいな感じだ。この手順を、データベースの設計と呼ぶ」

ししゃも丸「ほえー。データベースを使うためには、データの形を決めないといけないってこと?」

ニャンマルハウト「すべてのデータベースがそう、というわけではないぞ。世の中には、自由な形式のデータを扱えるドキュメント指向データベースというものもある。あるいは、キーと値のペアだけを高速でやりとりする、キーバリューストアというものもある。それぞれ得意なことが違うが……今回は、データを正確に取り扱いたいので、リレーショナルデータベースを使うというわけだ」

データベースの設計

ニャンマルハウト「じゃあ、データベースの設計に入ろうと思う。まず、Ridgepole を入れる」

ししゃも丸「Ridgepole?」

ニャンマルハウト「Ridgepoleは、データベースのスキーマを管理するためのツールで、オープンソースで公開されている。ruby で定義された DSL でデータベースのスキーマを記述し、コマンドを実行することで実際にデータベースサーバーのスキーマとファイルに記述したスキーマを同期できる」

ししゃも丸「そもそもスキーマってよくわかんないんだよね」

ニャンマルハウト「説明するとめっちゃ長くなるからすごい雑にいくけど、スキーマはデータがどんな形をしてるのか決めたルールみたいなものだ。さっき言った cats には name がある、みたいな部分」

ししゃも丸「ああ、なるほど。さっきは優しい説明だったけど、スキーマという専門用語があるんだな」

ニャンマルハウト「ちなみに、ソフトウェアを作っていく途中で、データの形を変えたくなる時がある。そういうときに、まずスキーマファイルを編集して、それをデータベースに反映する、という手順を取ると、いろいろ便利がいいわけだな」

ニャンマルハウト「Ridgepoleはgemで提供されているので、dbディレクトリはrubyの世界ということになる。Gemfile を定義して、ridgepoleをインストールしよう」

source 'https://rubygems.org'

gem 'ridgepole'
gem 'mysql2'

ニャンマルハウト「で、これをbundle install...。ちなみにrubyのバージョンは2.7.1にしたぞ」

ししゃも丸「俺はバージョンの数字が2つくらいのやつが丁度いいと思う」

ニャンマルハウト「なんのはなし?

ししゃも丸「数が多いと混乱する

ニャンマルハウト「……DBスキーマはなるべく適切な形で実装しておこう。アプリケーション層で定義すると、データベースだけで整合性の確保ができなくなったりするからな。もちろんRailsのようなフレームワークを使うなら、そちらに頼ったほうが合理的だ」

#!/usr/bin/env ruby

create_table 'accounts', force: :cascade, id: :binary, limit: 16, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci" do |t|
  t.string   'name',          limit: 128, null: false
  t.datetime 'registered_at',             null: false
end

create_table 'account_records', force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci" do |t|
  t.binary   'account_id',    limit: 16,  null: false
  t.datetime 'recorded_at',               null: false
  t.index ['account_id'], name: 'account_records_on_account_id', using: :btree
end

add_foreign_key 'account_records', 'accounts', name: 'account_records_ibfk_accounts'

ししゃも丸「読めないよ〜」

ニャンマルハウト「んー、そうだな。rubyちょっと勉強して、Ridgepoleのドキュメントに目を通せば、読めるようになると思うぞ。ちなみに、普通に英語として読み下してもある程度は意味が通るようになってる」

ししゃも丸「create_table は意味がわかる。ええと、accounts っていうテーブルと、account_records っていうテーブルがあるんだな。その他はちょっとわからないけど……」

ニャンマルハウト「いきなり全てを理解しようとしなくてもいい。だいたい雰囲気でなんとかなる

ニャンマルハウト「一応解説しておくと……accountsテーブルのプライマリキーはvarbinary(16) にした。128bitのデータは16byte(128÷8=16)になるからな。binary(16)でも適切に扱えば問題ない。ただ、Ridgepoleはbinaryを自動的にvarbinaryにするようなので、今回はvarbinaryとする」

Ridgepoleの挙動については細かい条件がありそうですが、ちょっと実装を見た感じだとよくわかりませんでした。もしかしたらAdapterとして使うmysql2の挙動かもしれません。

ニャンマルハウト「さて、実際にデータを挿入してテストしてみよう。MySQL 8.0 では、UUID_TO_BIN/BIN_TO_UUIDという便利な関数がある」

UUID_TO_BIN: UUIDを文字列表現した `xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx` 形式を、バイナリに変換する。

BIN_TO_UUID: バイナリをUUIDの文字列表現に変換する。

mysql> INSERT INTO accounts VALUE (UUID_TO_BIN("287f2209-6559-4a34-b086-f97f4e27d815"), "Nyanmalhout", NOW());
Query OK, 1 row affected (0.00 sec)

mysql> SELECT BIN_TO_UUID(`id`) AS pid, name, registered_at FROM accounts;
+--------------------------------------+-------------+---------------------+
| pid                                  | name        | registered_at       |
+--------------------------------------+-------------+---------------------+
| 287f2209-6559-4a34-b086-f97f4e27d815 | Nyanmalhout | 2020-07-22 17:33:10 |
+--------------------------------------+-------------+---------------------+
1 row in set (0.00 sec)

ニャンマルハウト「上手く動いてるみたいだ」

↑の例では、レコードの挿入時にUUIDの文字列表現をバイナリにし、読み出し時にバイナリを文字列表現にしています。

ししゃも丸(Zzz...

外部キー参照とアプリケーションレイヤー

ニャンマルハウト「さて、スキーマファイルを見た聡い技術猫であれば気づくだろう。外部キー参照が使われている。以下の1行だ」

add_foreign_key 'account_records', 'accounts', name: 'account_records_ibfk_accounts'

ニャンマルハウト「これは、account_records の account_id カラムは、accounts のプライマリキーと対応していますよ、という意味だ。プライマリキーというのはテーブル内で重複しないことが保証されていて、絶対にデータを入れなければならないこともルールになっている。そのため、account_records の各データ(レコードという)は、accounts と必ず紐づくというわけだ」

ニャンマルハウト「account_records が accounts のレコードの履歴を保存するという意味を考えれば、accounts のない account_records が存在しないことは当たり前なんだが、こういう当たり前をきちんとデータベースに教えてやる必要があるというわけだな」

こういったデータの特性をデータベースレベルで表現するか、アプリケーションレベルで表現するか、というのは一長一短あります。

・仮想政府の公共サービスAPIという信頼性が求められること
・アプリケーション層を薄いフレームワークで実装する計画であること
・アプリケーション層の寿命よりデータ構造の寿命のほうが長い可能性が十分考えられること

以上の理由から、今回は原則的にデータベース側にデータ構造の情報を持たせる方針で設計していきます。

今回の作業はこのコミットにまとめられています。

https://github.com/niaeashes/nyarnia/commit/14ba533f67f7bc6917d3b460c0f0ddd540780695

次回予告

ニャンマルハウト「ここまででデータベースの最低限の設計はできた。次回は実際にデータを操作するAPIのプログラムを書いていくぞ」

ししゃも丸「やっとかあ〜。下準備だけでめちゃくちゃ長いんだね」

ニャンマルハウト「いや、そうでもない。慣れた技術猫はここ3日間の工程くらいなら30分で終わらせるし、俺の飼い主でさえ数時間で終わらせるからな」

ししゃも丸「ひえ〜」

ようやく下準備が終わったニャルニア国の公共事業サービス、次回はAPIのプログラムを書いていきます。

ニャンマルハウト「ちなみに俺はTypeScriptはあんまり好きじゃない」

この記事が気に入ったらサポートをしてみませんか?