はじめに
最近、オライリー・ジャパンの「SQLアンチパターン」という本を読んでいます。
前回の記事に引き続き、今回は「EAV」というアンチパターンについて、私なりの実例を交えながら解説したいと思います。
なお、本記事で紹介している実装例やデメリットの説明は、書籍の内容をそのまま転記したものではありません。書籍の内容を参考にしながら、私なりの解釈で再構成したものとなっています。
参考著書
オライリー・ジャパン「SQLアンチパターン」
https://www.oreilly.co.jp/books/9784873115894/
EAVとは
EAV(Entity-Attribute-Value)は、エンティティ(Entity)、属性(Attribute)、値(Value)の3つの要素でデータを管理するデータベース設計パターンです。
具体的には以下のような構造になります。
- Entity(エンティティ): データの主体となるもの(例:商品、ユーザーなど)
- Attribute(属性): エンティティが持つ特性の名前(例:色、サイズ、価格など)
- Value(値): 属性の具体的な値(例:赤、L、1000円など)
このパターンの特徴は、データベースのスキーマを固定せずに、新しい属性を自由に追加できる「柔軟性」にあります。
例えば、商品管理システムで新しい商品属性を追加する際に、テーブル構造を変更する必要がありません。
しかし、この柔軟性は同時に多くの問題を引き起こす可能性があり、データベース設計においてアンチパターンの一つとして認識されています。
問題のある実装例
以下は、ECサイトでの商品管理システムでEAVパターンを使用した例です。
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE product_attributes (
product_id INT,
attribute_name VARCHAR(50),
attribute_value TEXT,
PRIMARY KEY (product_id, attribute_name),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- サンプルデータ
INSERT INTO products VALUES (1, 'カジュアルシャツ');
INSERT INTO product_attributes VALUES
(1, 'サイズ', 'M'),
(1, '色', 'ネイビー'),
(1, '素材', 'コットン100%'),
(1, '袖丈', '長袖');
この設計では、商品の属性を柔軟に追加できるように、属性名と値を別テーブルで管理しています。
デメリット
1. データ整合性の確保が困難
- 属性値の型チェックができない(全てTEXT型として保存)
- 必須属性の制約を設定できない
- 属性値の範囲チェックが困難
2. 属性名の補完と行の再構築が必要
- 属性名を補わなければならない
- 行を再構築しなければならない(1つのエンティティの情報が複数の行に分散するため)
解決策
1. シングルテーブル継承の採用
全ての属性を1つのテーブルに定義する方法です。
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100),
size VARCHAR(10),
color VARCHAR(30),
material VARCHAR(50),
sleeve_length VARCHAR(20)
);
この方法は以下の場合に適しています。
- サブタイプ(トップス、ボトムスなど)の数が少ない場合
- 各サブタイプ固有の属性の数が少ない場合
- Active Recordのような単一テーブルに対するデータベースアクセスパターンを使用する必要がある場合
- シンプルなCRUD操作が主な用途である場合
ただし、以下の点に注意が必要です。
- サブタイプやその固有属性が増えると、NULL値が多くなる
- 新しいサブタイプの追加時にはテーブル構造の変更が必要
- データベース製品によっては列数の上限(例:PostgreSQLは1600列)に達する可能性がある
2. 具象テーブル継承の採用
各サブタイプごとに独立したテーブルを作成し、それぞれのテーブルに必要な属性をすべて持たせる方法です。
共通属性も各テーブルで重複して定義します。
CREATE TABLE tops (
product_id INT PRIMARY KEY,
size VARCHAR(10),
sleeve_length VARCHAR(20),
neckline_type VARCHAR(30),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
CREATE TABLE bottoms (
product_id INT PRIMARY KEY,
size VARCHAR(10),
inseam_length INTEGER,
rise_type VARCHAR(30),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
この方法は以下の場合に適しています。
- サブタイプごとに異なる属性を持つ場合
- サブタイプ固有の処理やバリデーションが多い場合
- サブタイプごとに独立してデータを管理したい場合
ただし、以下の点に注意が必要です。
- 共通属性に変更がある場合、すべてのテーブルを修正する必要がある
- サブタイプ横断的な検索を行う場合、UNIONが必要になる
- データの重複(共通属性)が発生する
3. クラステーブル継承の採用
共通属性を持つベーステーブルと、サブタイプ固有の属性を持つ個別のテーブルを作成し、外部キーで関連付ける方法です。
-- 共通属性を持つベーステーブル
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100),
size VARCHAR(10),
color VARCHAR(30),
material VARCHAR(50)
);
-- トップス固有の属性を持つテーブル
CREATE TABLE tops (
product_id INT PRIMARY KEY,
sleeve_length VARCHAR(20),
neckline_type VARCHAR(30),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- ボトムス固有の属性を持つテーブル
CREATE TABLE bottoms (
product_id INT PRIMARY KEY,
inseam_length INTEGER,
rise_type VARCHAR(30),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
この方法は以下の場合に適しています。
- 共通属性とサブタイプ固有の属性が明確に分かれている場合
- データの正規化を重視する場合
- オブジェクト指向的な設計との親和性を重視する場合
ただし、以下の点に注意が必要です。
- サブタイプのデータを取得する際に常にJOINが必要
- 外部キー制約の管理が必要
4. 半構造化データの採用
リレーショナルデータベースの柔軟性を高めるために、JSON等の半構造化データ型を活用する方法です。
-- PostgreSQLの場合(JSONB型)
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100),
attributes JSONB
);
-- MySQLの場合(JSON型)
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(100),
attributes JSON
);
この方法は以下の場合に適しています。
- 属性の種類や構造が頻繁に変更される場合
- スキーマレスな柔軟性が必要な場合
- プロトタイピングや要件が流動的な開発初期段階
- NoSQLデータベースへの移行を検討している場合
ただし、以下の点に注意が必要です。
- 型の安全性が低下するため、データの整合性チェックはアプリケーション側で実装する必要がある
まとめ
「属性を柔軟に追加・変更できるようにしたい」という要件に対して、EAVパターンを採用することは一見理にかなっているように見えます。
しかし、EAVパターンには様々なデメリットが潜んでいることがわかりました。
特に以下の点について、具体的な実装例を通じて理解を深めることができました。
- データ型の制約が失われることで、アプリケーション側での実装負担が著しく増加すること
- 一見柔軟に見える設計が、かえってシステムの拡張性を損なう可能性があること
重要なのは、「柔軟性」という言葉に惑わされず、実際の要件や運用を見据えた設計を選択することが大切なのだと学びました。