言わずと知れた「リーダブルコード」という書籍があります。私も一応読んでます。要するに綺麗で読みやすいコードは、未来の自分を含めた他の開発者に優しいというお話ですね。
「リーダブルコード」では特に触れられていませんでしたが、スキーマつまりデータベースのテーブル名やカラム名もコードの一種ですよね。そしてスキーマも開発者が日々目にするものです。スキーマが綺麗で読みやすいと、開発者に優しくなれますよね。どういうスキーマが開発者に優しくなるのかを考えてみたいと思います。
また、長く使われているデータベースは改修の積み重ねによってスキーマにも技術的負債がたまっていきます。この技術的負債を返済するためのスキーマリファクタリングについても併せて考えます。
なお、私は MySQL しか経験がないので、以下の記述は MySQL での事情を前提にしています。他の RDBMS では違った話になることもあるかと思いますが、その点はご了承ください。
リーダブルなスキーマ
命名は具体的な内容を表す
a
とか tmp
とか、なんの情報も持ってない命名は流石に滅多にないとは思いますが皆無でもありません。data
とか status
のように、一見すると意味がありそうだけど、具体的には何も説明していない命名はいくらでも見かけますね。
(ほげほげ)2
とか (ふがふが)new
など、もとになったテーブル/カラムと何が違うのか何も説明していない命名は滅茶苦茶見かけます。
このような命名はせずに、具体的な意味を表す命名をしましょう。
日本語/ローマ字問題
具体的な命名といっても、和英辞書をひいてみたら初めて知った単語が出てきてしまうということもあります。自分が知らないだけなら自分の無知ですが、他の開発者に聞いても知らない単語だった場合、果たしてその単語を使って具体的な意味を表せるのかどうかは疑問ですよね。
また、業務システムなどを開発している場合業界特有の用語が出てきます。こうした単語は訳語が存在しても一般に知られてないことも多いですし、日本特有の業界であればその業界用語は英訳が存在しないこともあるでしょう。
こうした場合には、無理に英語の命名をするよりも日本語やローマ字の命名をしたほうがかえって分かりやすくなる場合もあるかと思います。
一つのテーブル/カラムに複数の情報を持たせない
アプリケーションコードにおける単一責任に近しい話ですね。一つのテーブル/カラムには単一の情報のみを格納するようにする。逆に、一つのテーブル/カラムにまとめるべきものは、過剰に分割しないようにしましょう。どうしたらいいかあやふやになってしまうということはモデリングが不十分ということですから、そこからやり直す必要があります。
また、改修時に格納すべき情報が増えて、それを既存のテーブル/カラムに詰め込んでしまうというパターンもありますが、これは厳に慎むべきでしょう。
idとコード、フラグとステータス
以前に記事を書きましたので詳細はリンク先を参照いただくとして、要約としては id とコード、フラグとステータスはそれぞれ意味が違うのだから、混同してはいけないという話です。
最初はフラグとしてスキーマ設計したけれど、改修によってステータスに変更したというケースはあります。その場合は既存のカラムを使いまわすのではなく、後述するスキーマリファクタリングを行うべきでしょう。
コメントをつけるか
データベースの機能として、テーブルやカラムにコメントを付けることができます。これを付けるかどうか。
ドキュメントを別途作成するから、データベース自体にはコメントは不要というのは一理あると思います。データベースのコメント欄にはそれほど多くの情報を書けないのに対して、別途作成するドキュメントであればいくらでも説明は出来ますし、図なども入れ放題ですしね。ただし、ドキュメントが常に最新に更新される体制がずっと維持できるのが前提となります。
一方、データベースにコメントを入れる場合、DB クライアントなどで閲覧したときに常にコメントも表示されるというメリットがあります。いちいちドキュメントを別途開いて参照しなくて済みますので便利ですね。また、DB クライアントの機能としてスキーマを読んでドキュメントを自動生成できますので、人間がいちいちドキュメント更新しなくて済むという話もあります。こちらも、いつの間にか誰もコメントを更新しなくなるということがありますから注意が必要です。
データベースにコメントを入れる場合に、全カラムに入れるかどうかという観点があります。サロゲートキーなど自明なカラムにはいちいちコメントを入れなくても分かるだろうという話ですね。ただ、自明であるかどうかは主観が入りますので、全カラムにコメントを入れるルールにするか、コメントしなくてよいカラムを明示しておく方がスキーマの治安が良くなるかなと個人的には思います。
型を細かく設定する
例えば郵便番号を格納する場合、char(7) となっていればより明確になりますね。もしも char(8) になっていればハイフンも含めて格納するということが開発者に伝わります。これが varchar(255) だとすると、開発者はハイフンをどうするのかいちいち確認しなければなりません。
ビュー/トリガー/ストアドプロシージャを使うか
ビューはともかく、トリガー/ストアドプロシージャを使うかどうかはアプリケーションの設計に関わってくる話なので、スキーマ設計からは少し外れるかもしれません。もしくは設計者の好みの出てくるところですかね。
使うか使わないかに善悪はないのですが、使うか使わないかのルールは統一した方が良いと思います。こっちの処理はストアドプロシージャで行われて、あっちの処理はアプリケーションコードで行われるとなっていると、処理を探すのに手間取ってしまいます。
制約をかけるか
UNIQUE 制約、NOT NULL 制約、外部キー制約などですね。データベース自体で制約をかける以外に、アプリケーション側でバリデーションとして実現する方法もあります。
一般にアプリケーション側でバリデーションした方が柔軟なルールが設定できます。また、データベースに制約をかけるとデータベースが重くなるので、少しでもデータベースの性能をあげるるために制約をかけないという考え方もあります。
一方、そのデータベースに接続するアプリケーションが複数ある場合、その全てのアプリケーションでバリデーション処理を統一するのは大変なので、データベース側で制約を持たせた方がシンプルになるという判断もあると思います。
また、制約はスキーマ情報の一つとして開発者に対するメッセージになりますので、リーダブルという意味では制約設定があった方がいいと思います。
1 対 1 のテーブルを作成するか
1 対多、多対多のテーブルは正規化の結果として必然的に定義されるのですが、1 対 1 のテーブルは決まるとは限りません。論理的に 1 対 1 のテーブルを定義する意味がない場合、リーダブルになるかどうかが決め手になります。分けた方が理解しやすいのであれば分けます。
ナチュラルキーかサロゲートキーか
リーダブルという観点からはナチュラルキーの方が優れているでしょう。まさにスキーマにそのテーブルで何が重要であるかが書かれているわけですから。ただし、システムの成長に伴ってナチュラルキーが変更になる場合があり得ます。その場合はスキーマリファクタリングが必要です。
一方、アプリケーションが使用しているフレームワークでサロゲートキーを使うことが必須となっていればサロゲートキーを使わざるを得ません。
作成日時/更新日時カラムを作成するか
トランザクションテーブルであれば作成日時/更新日時カラムはあった方がよいでしょう。一方、マスターテーブルでは必須というわけではないですね。
あえて作成日時/更新日時カラムを作らないことで、これはマスターテーブルであるということを開発者に対して説明できるという考え方もあると思います。
インデックス
どのカラムにインデックスを付けているかで、そのテーブルがどういう検索を想定しているかの開発者に対するメッセージになりえます。あればいいんだろうということで全カラムにインデックスが設定されているスキーマを見ることがありますが、少なくともリーダブルではないですね。
テーブル名/カラム名に比べるとインデックス名は一般に意識されることは少ないと思いますが、複合インデックスでは検索意図を明示した命名にしておくとリーダブルになります。
統一性を持たせる
ここまでいろいろなリーダブルスキーマのルールをあげましたが、こうした方が良いというものと、どちらでも構わないというものがありました。どちらでも構わないものについても、同一スキーマの中ではどちらかに統一をしておくべきでしょう。ルールがばらばらだと開発者は混乱しますし、スキーマを改修する際にもどっちにするかがあやふやになってしまいます。
スキーマリファクタリング
リファクタリングの考え方自体はアプリケーションコードもスキーマも変わりません。不適切になっている部分を適切なものに変えていく。そして行為をずっと続けていくわけです。
ただし、アプリケーションコードに比べて、スキーマはリファクタリングにあたって特有の困難がいくつかあります。それぞれ、パターン別に考えてみたいと思います。
テーブルを削除する
これは一番簡単です。そのテーブルを参照するアプリケーションコードを全て削除した後、テーブルを削除します。実際にはテーブルにアクセスがなくなったかどうか、データベースのログで確認が必要ですが。
カラムを追加する
カラム追加ではアプリケーション側の考慮は不要なのですが、データベース側の考慮が必要です。具体的には、レコード数が膨大な場合、カラム追加にも時間がかかってしまうということです。その間、最悪アプリケーションの実行が止まってしまいますので注意が必要です。
アプリケーションへの影響を最小限にする方法もいくつかあります。そのあたりについて過去記事に書きましたので参照ください。
カラムを変更する
カラムを変更するといっても、カラム名を変更する、カラム型を変更する、カラムコメントを変更するがあります。
一番楽なのはカラムコメントを変更することです。アプリケーションへの影響はありませんので、がんがんやって構いません。
カラム名の変更はデータベース的には簡単です。ALTER TABLE 自体は一瞬で終了します。問題は、カラム名の変更をデータベースとアプリケーションで完全に同期するのが困難ということです。デプロイ方法の問題ですね。
データベースを先に変更した場合、アプリケーションは存在しなくなった旧カラム名でアクセスしてエラーになります。アプリケーションを先に変更した場合、アプリケーションはまだ存在しない新カラム名でアクセスしてエラーになります。
メンテナンスタイムとしてアプリケーションを停止してよいなら問題ありません。また数秒間程度ならエラーが発生しても許容できるというサービスなら大丈夫です。しかし、許容できない場合は以下の手順をとります。
- 新名称のカラムをテーブルに追加する。
- 旧名称のカラムから新名称のカラムにデータをコピーする。
- アプリケーションを新名称カラムを参照するように変更してデプロイする。
- 旧名称カラムを削除する。
1番目の手順ではカラム追加の際の手順を踏まなければなりません。また、4 番目の手順では後述のカラム削除の手順を踏む必要があります。
カラムの型変更はカラム追加と同様にレコード数が膨大な場合に時間がかかります。また、新旧の型に互換性が無い場合はカラム名変更と同様にアプリケーションがエラーになりえますので、カラム名変更と同様の手順が必要になります。
カラムを削除する
カラムを削除する場合、カラム追加と同様に巨大なテーブルでは時間がかかるという問題があります。
また、アプリケーションコードからカラムの参照を全て削除していても、フレームワークがスキーマ情報をキャッシュしている場合があります。この場合、カラムを削除してしまうとフレームワークが削除済カラムにアクセスしようとしてエラーになってしまいます。対策として、以下の手順が必要になります。
- アプリケーションコードにカラム無視の設定(ActiveRecord の場合は ignored_columns)を入れてデプロイする。
- データベースからカラムを削除する。
- アプリケーションのカラム無視設定を除去する。
スキーマリファクタリングは手間がかかる
テーブル削除は簡単ですが、カラムは追加・変更・削除ともに非常に手間がかかります。アプリケーションコードのリファクタリングですら新機能開発優先で後回しにされがちですから、スキーマのリファクタリングがなかなか進まないのも無理はないのかもしれません。
最初からリーダブルなスキーマで設計しておけばリファクタリングなどしなくて済むのだというのは一つの理想でしょう。とはいえ人間はそんな完璧な存在ではありませんから、どうしたって行き届かないところは出てきます。その行き届かなかったところは、見つけたら後からでも手直ししたいですよね。だからって最初の設計をいい加減にしていいという話ではないのですが。
また、もしも最初の設計が完璧に出来たとしても、システムの成長に従って要件も変わってきます。当初の設計は当然拡張性を見越したものにしておくべきですが、それでも未来の全てを見越すことは出来ません。拡張に拡張を重ねた結果、スキーマに無理がかかってきた状態を技術的負債というわけですから、その負債は解消したいものですね。
まとめ
この記事で書きたかったことを要点としてまとめると以下になります。
- スキーマだってコードの一種だ。
- リーダブルなスキーマは開発者に優しい。
- スキーマにも技術的負債はたまる。
- 技術的負債がたまったらスキーマもリファクタリングしよう。
アプリケーションコードと共に、スキーマとも仲良くしていきたいですね。