EAV(エンティティ・アトリビュート・バリュー)

テーブル設計の変更が起きると、それを使用するプログラムに大きな影響を与えることから、出来る限りテーブル設計の変更を避けようという意識が働く事が多い。よって、柔軟な設計にするため、より抽象度の高いテーブルを設計しようとする。ただそのデメリットも大きいというお話である。
抽象度を上げるためにここで行なっていることは、テーブル(エンティティ)自体に属性(アトリビュート)の属性名とその値(バリュー)のセットを格納する方法である。本書ではIssuesというテーブルを作っているが、属性値が存在せず、IssuesAttributesというテーブルに属性名と値が入っている。
動機として挙げられているのはサブタイプの実装である。サブタイプとはオブジェクト指向モデルでいうところのサブクラスとほぼ同じであり、メソッドを持っていない違いがあるくらいである。つまり、親側には共通する属性を、子側にはそれぞれの固有の属性をもたせるようにERモデルは記述することができる。これを実装する場合の一つの選択肢である。

なぜアンチパターンなのか

必須属性を設定できない

普通に設計していればNOT NULLと設計してしまえば必須属性になるのだが、このような設計をすると、必須属性を定義できない。アプリ側で頑張るしかない。

SQLのデータ型を使えない

値を文字列で統一してしまうと、データ型が文字列で統一されてしまう。日付型や数値型が設定できず、不整合が起きてもわからない。そこで、属性テーブルに日付型の値を格納したり数値型の値を格納する属性を追加することもできるが、SQLが更に複雑になってしまう。

参照整合性制約を強制できない

属性の中に外部キーがあっても、参照整合性制約は設定できない。

属性名を補わなくてはならない

属性名が値になっているため、一貫性が保てないとき問題になるというもの。属性を動的に追加することはとてもいい事なのだが、その反面、同じ値に違う属性名を設定してしまうという問題が発生する。このため、属性名を格納する属性(意味がわかりにくいですね)を登録しておくマスタを作って、その外部キーにすることにより、入力制限をかけるという対応をすべきだろう。
本書では、随時その場で追加ができないとしている。しかし、マスタに随時属性名をを入れれば問題ないと思うのだが、なぜこのような主張をしているのかわからない。むしろ、外部キーにして入力制限をかけたとしても、気軽にマスタに属性名を追加できるような仕組みになっていた場合に、同じ値に違う属性名を設定してしまう危険性は、運用上、いずれにせよ避けられない。

行を再構築しなければならない

結果を作成する際に属性を列にマッピングするのが大変、という指摘。そのとおり。

解決策

EAVを使わずに、サブタイプを実装する方法については以下のような方法がある。

シングルテーブル継承

すべてのサブタイプを一つのテーブルに格納し、該当するサブタイプのときだけ設定する属性と設定しない属性を混在させるやり方である。極めて一般的な実装方法である。ただし、本書にもあるように、属性とサブタイプの対応関係はアプリ側で負担することになる。サブタイプの固有の属性が少ない場合は適切な選択となる、とあるが、そのとおりである。

具象テーブル継承

サブタイプごとにテーブルを作る方法。親側として検索したい場合は、サブタイプすべてのテーブルを横断して検索しなければならない。そこで、各テーブルをUNIONでつなぎ、共通項目を検索すVIEWを定義すると、親側のテーブルが存在するように見える。

クラステーブル継承

サブタイプも親側もどちら側もテーブル化してしまう方法。通常、サブタイプだけで構成されるテーブルだけで完結する検索は少ないため、親とサブタイプをJOINしたVIEWを作ることになる。シングルテーブル継承の逆と考えて良い。なお、Postgresqlではこのようなテーブルを作ることもできる。

半構造化データ

ラージオブジェクトにXMLなどの半構造化データとして格納する方法。新しい属性を随時追加するにはこの方法を使うこともできるが、SQLでは取れないという大きな欠点がある。

本当に使うべきではないのか?

この本ではこの使用を正当化する理由は難しいとある。しかし、そんなことはない。たしかに、サブタイプの実装ではEAVを使ってはいけない。先ほどの解決策のうちの前3つのうちのどれかを使用するべきである*1
しかし、随時追加する構造として解決策に指定されている半構造化データであるが、これはSQLでは使えない。よって、このような設計こそRDBMSを使う意味を失わせると思う。それよりはEAVのほうがまだいい。例えば、スペック(仕様)を格納するテーブルである。スペックは「項目」と「値」からなりたつ。これは製品ごとに違う。このため、すべての製品に共通するようなスペックのテーブルを作ってしまうと巨大になり、NULLばかりが入るテーブルができてしまう。製品種別ごとにテーブルを作ったとしても、異なる製品種別ができた時にどうするという問題が生じる。よって、このような場合に使用するのが望ましい。さらに、EAVのテーブルには並び順と大項目(大分類)といった属性もあると便利だ。また、製品ごとに指定して良い属性とオプションで設定できる属性、設定できない属性を登録するマスタもあったほうがよい。そのようなデータはDB上では制限をかけることはできないが、アプリ上でチェックする際にとても便利であり、同じ値に違う属性名を入れてしまう危険性を避ける事ができる。

*1:大学院の時の授業とは名前が違うだけで、同じことを言っていた