LESSON 15分

ストーリー

「設計レビューで『これ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分