ストーリー
「設計レビューで『これEAVパターンですね』と言われたんですが、何が問題なんですか?」
高橋アーキテクトはため息をついた。「EAVは”柔軟性”の名の下にすべてを犠牲にする。型安全性、制約、クエリ性能…。知らずに使うと、後で地獄を見ることになる」
アンチパターン1: EAV(Entity-Attribute-Value)
危険な設計
-- EAV: すべてを「属性名-値」のペアで格納
CREATE TABLE entity_attributes (
entity_id INT NOT NULL,
attribute_name VARCHAR(100) NOT NULL, -- 'color', 'size', 'weight'...
attribute_value TEXT, -- すべて文字列に...
PRIMARY KEY (entity_id, attribute_name)
);
何が問題か
| 問題 | 説明 |
|---|---|
| 型安全性の喪失 | すべてTEXT型。数値も日付も文字列に |
| 制約が効かない | NOT NULL, CHECK, FK制約が使えない |
| クエリが複雑 | 1エンティティの情報を得るのに大量のJOINかPIVOT |
| パフォーマンス劣化 | 行数が爆発し、インデックスも効きにくい |
正しい設計
-- 属性が固定なら: 通常のカラム
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
color VARCHAR(50),
size VARCHAR(20),
weight DECIMAL(8, 2)
);
-- 属性が可変なら: JSONB(PostgreSQL)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
attributes JSONB NOT NULL DEFAULT '{}'
);
-- JSONB なら型チェック付きで検索もできる
SELECT * FROM products
WHERE attributes->>'color' = 'red'
AND (attributes->>'weight')::decimal > 1.0;
アンチパターン2: ポリモーフィック関連
危険な設計
-- 1つの外部キーで複数テーブルを参照
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
body TEXT NOT NULL,
commentable_type VARCHAR(50) NOT NULL, -- 'Post', 'Photo', 'Video'
commentable_id INT NOT NULL -- FK が効かない!
);
何が問題か
- FK制約が使えない: commentable_id の参照先が不定
- JOINが複雑: type によって結合先が変わる
- 整合性が保証されない: 存在しないIDを指せてしまう
正しい設計
-- 方法1: 個別のFK
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
body TEXT NOT NULL,
post_id INT REFERENCES posts(id),
photo_id INT REFERENCES photos(id),
video_id INT REFERENCES videos(id),
CHECK (
(post_id IS NOT NULL)::int +
(photo_id IS NOT NULL)::int +
(video_id IS NOT NULL)::int = 1
)
);
-- 方法2: 共通親テーブル(推奨)
CREATE TABLE commentables (
id SERIAL PRIMARY KEY,
type VARCHAR(50) NOT NULL
);
CREATE TABLE posts (
id INT PRIMARY KEY REFERENCES commentables(id),
title VARCHAR(200) NOT NULL
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
body TEXT NOT NULL,
commentable_id INT NOT NULL REFERENCES commentables(id)
);
アンチパターン3: マジックナンバー/文字列
-- NG: マジックナンバー
CREATE TABLE orders (
status INT NOT NULL -- 0=pending, 1=paid, 2=shipped... コード以外で意味不明
);
-- OK: ENUMまたはCHECK制約
CREATE TABLE orders (
status VARCHAR(20) NOT NULL
CHECK (status IN ('pending', 'paid', 'shipped', 'delivered', 'cancelled'))
);
アンチパターン4: God Table(神テーブル)
-- NG: なんでもテーブル
CREATE TABLE everything (
id SERIAL PRIMARY KEY,
type VARCHAR(50), -- 'user', 'product', 'order'...
field1 TEXT,
field2 TEXT,
field3 TEXT,
field4 TEXT,
-- 大量のNULLカラムが並ぶ
);
関連する属性を適切なテーブルに分割すること。
アンチパターン5: Soft Delete の罠
-- よくある実装
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
deleted_at TIMESTAMP -- NULLなら有効、値があれば論理削除
);
-- 問題: すべてのクエリにWHERE deleted_at IS NULLが必要
-- UNIQUE制約が論理削除レコードと衝突する
-- 改善: 状態カラム + 部分インデックス
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended', 'deleted'))
);
-- active なユーザーのみのユニーク制約
CREATE UNIQUE INDEX idx_users_email_active
ON users(email)
WHERE status = 'active';
まとめ
| アンチパターン | 問題 | 対策 |
|---|---|---|
| EAV | 型安全性・制約の喪失 | 通常カラム or JSONB |
| ポリモーフィック関連 | FK制約が効かない | 共通親テーブル or 個別FK |
| マジックナンバー | 意味が不明 | ENUM / CHECK制約 |
| God Table | 大量のNULL、意味不明 | テーブル分割 |
| 安易なSoft Delete | クエリ複雑化、制約衝突 | 状態カラム + 部分インデックス |
理解度チェックリスト
- EAVパターンの問題点を3つ以上挙げられる
- ポリモーフィック関連の代替手法を説明できる
- 各アンチパターンを設計レビューで指摘できる
次のステップ
Step 1 の総まとめとして理解度チェッククイズに挑戦しよう。正規化、ER図、データ型、アンチパターンの理解を確認する。
推定読了時間: 15分