データベース設計をすっきりさせる
データベースの正規化とは
正規化(Normalization)とは、データベース設計の手法の一つで、データの冗長性を排除し、一貫性と整合性を保つために、データを適切に分割するプロセスのことである。
具体的には:
-
NULL を含まない設計
記事では「カラムに NULL を含まないのがデータベース設計の基本」と述べられています。これは、NULL があるとデータの一貫性が損なわれ、処理が複雑になるためです。
-
テーブルの分割(正規化のアプローチ)
必要なカラムを見つけたら、それを 別のテーブルに分割 することで正規化を進める方法が述べられています。
例えば、特定のカラムにだけ NULL が入りうる場合、そのカラムを持つレコードを 別のテーブルに分ける ことでデータの正規性を保つことができます。 -
NULL 逃れをしない(データの適切な表現)
「unknown」や「9999」などのダミー値を使って NULL を回避するのではなく、適切なテーブル構成を考えるべきであると述べられています。
入力が必須でないデータは、別のテーブルとして管理するべき という考え方です。データベースの正規化戦略 -- いかに Null を避けるか
💡 テーブルの分割(正規化のアプローチ)
データベースの設計では、「NULL が入りうるカラム(列)」がある場合、データの管理が複雑になりやすいです。そこで、NULL を含まないようにテーブルを分割することでデータの正規性を保つ という方法が推奨されています。
💡 具体例で理解しよう
👎 悪い設計(NULL を含むテーブル)
例えば、ある会社の 社員情報 を管理するデータベースがあるとします。
社員 ID 名前 メールアドレス 電話番号 1 田中 tanaka@xyz.com 090-1111-2222 2 佐藤 sato@xyz.com NULL 3 鈴木 NULL 080-3333-4444 4 高橋 takahashi@xyz.com NULL このような設計では、電話番号がない社員 や メールアドレスがない社員 のカラムに NULL が入ってしまいます。
これが問題になるのは、以下のようなケースです:-
NULL
を含む演算(例:メールアドレスの検索)で、意図しない結果になる -
NULL
の場合の処理が増えてプログラムが複雑になる -
NULL
を考慮したデータ操作をしないとバグの原因になる
👍 良い設計(テーブルを分割して正規化)
上記の問題を避けるために、メールアドレスと電話番号を別のテーブルに分ける ことで、データの整合性を保ちつつ、NULL をなくすことができます。
1. 社員の基本情報テーブル(NULL なし)
社員 ID 名前 1 田中 2 佐藤 3 鈴木 4 高橋 2. メールアドレス管理テーブル(NULL なし)
社員 ID メールアドレス 1 tanaka@xyz.com 2 sato@xyz.com 4 takahashi@xyz.com 3. 電話番号管理テーブル(NULL なし)
社員 ID 電話番号 1 090-1111-2222 3 080-3333-4444
✨ まとめ
- 悪い設計 → NULL が含まれるテーブルは、データの管理が複雑になりやすい
- 良い設計 → NULL を含みそうなカラムを別のテーブルに分割 することで、データの整合性を保てる
-
一意性制約
データの一意性とは、そのデータが唯一であることを指す。
重服による不具合を防ぐため、データはできるだけ一意であるべきである。
PRIMARY KEY や UNIQUE 制約を課すことで、データの一意性を保つことができる。
一意性制約を課すべきデータの例
- ユーザー ID
- メールアドレス
- 電話番号
- ユーザー ID & プロジェクト ID の組み合わせ: ユーザーを同一のプロジェクトに複数回登録することはできない
とりあえず一位性制約と NOT NULL 制約を課す
データベースの正規化を目指すために、とりあえず一位性制約と NOT NULL 制約を課してみるのも有効である。
これを行うことで、自然に正規化の方向へと進んでゆく。
もし、制約を追加できないのなら、テーブルを小さく分割することで、正規化を進めることができる。
外部キー制約
複数テーブル間の整合性を保つためには外部キー制約を活用すると良い。
外部キー制約とは
- A テーブルに社員 ID と部署 ID のペアを登録する
- 部署 ID に B テーブル(部署 ID カラム)を外部キーとして課す
- B テーブル(部署カラム)に存在する部署 ID のみ A テーブルに登録することができる。
コトに注目するデータベース設計
記録のタイミングが違うデータはテーブルを分ける
- 例えば、同じテーブル内で、注文日と配送日を記録しようとする
- 注文日は確定しても、配送日は確定しない
- NULL を許容する必要が出てくる
- 別のテーブルに分けることで NULL を避けることができる
記録の変更を禁止する
- データベースにおいて、過去の記録の変更(UPDATE)は御法度
- 過去のデータが消えるため、変更前の金額が分からなくなる
- 誤った変更をしても、元のデータを復元できない
- 監査が必要な場合、どのタイミングで変更されたか追跡できない
- データの追加(INSERT)で代替する
- 誤ったデータを直接修正せず、「取消」のデータを追加する
- 新しいデータを「新規」として登録する
- これにより、変更履歴が明確になり、誤ったデータも追跡可能
参照をわかりやすくする工夫
コトを記録すると同時に状態も更新する
-
コトを記録するだけでは、現在の状態が分からない
- 入金、出勤の記録だけでは残高が分からない
-
コトを記録する度に、現在の状態も更新する
- 入金、出勤の度に、残高を再計算し更新する
- 古い残高は DELETE し、新しい残高を INSERT する
- 残高は二次的な情報なので、入出金が成功した後に更新でよい
- 同時更新である必要がないので、別サーバで管理することもできる(非同期メッセージング)
イベントソーシングとは
- イベントの記録の積み重ねから、派生する情報を目的別に記録する方式
- 入出金の記録の積み重ねから、残高や入金の合計、出金の合計、出金の増加率などを作成する
オブジェクト設計とテーブルの設計
オブジェクトとテーブルは1対1?
- オブジェクトとテーブルは1対1と思えることがある
- どちらも業務の関心ごとを表しているわけだから、ある意味では当たり前である
- しかし、オブジェクトとテーブルは存在する目的が違う
- 似ているだけで別物なので、1対1でなければならないわけではない
- したがって、一方を変更したからといってもう一方も変更しなければならないわけではない
MyBatis SQL Mapper について
- オブジェクトとテーブルのマッピングの仕組みとして、さまざまなフレームワークが存在する
- しかし、機械的なマッピングはドメインオブジェクトに不要な要素をもたらすことになる
- クラス名やメソッド名の規則
- 不要なメソッドの追加
- アノテーションの追加
- ドメインオブジェクトの設計の自由度を保ちやすいフレームワークを使用するべき
- MyBatis SQL Mapper がオススメ