EAVコンテナ
「エンティティ-アトリビュート-バリュー (Entity-attribute-value) モデル (EAV) は、潜在的には大変に多くの属性(プロパティやパラメータ)を記述できることが必要だが、 通常は特定のエンティティに適用される数はさほど多くない場合に適したデータモデルです。 数学ではこのモデルは疎行列として知られています。 EAVはまた、オブジェクト-属性-値モデル、垂直データベースモデル、オープンスキーマなどと呼ばれることもあります。」
出展: ウィキペディア(英語).
はじめに
ほとんどの人は、上記の最初の段落で迷子になるので、例を上げてこれを説明して行きましょう。
エンティティ (レコードを表す ORM 用語) が子レコードに対して関連するたくさんの属性を持つ時には、通常 EAV のコンテナを使用します。 しかし全レコードを見ると、それらの属性はレコードごとに異なっているかも知れません。 これは属性名をそのままカラム名にしたエンティティテーブルを作ることは不可能であることを意味しています。 なぜならあまりにも多く存在するだろうカラムのうちほとんどはデータを持つことはないからです。 また列を事前に定義する必要が有るため、動的属性で対処することもできません。
この問題を「リレーショナル」スタイルで解決するために、「1 対多 (One-to-Many) 」の関係でエンティティに対する子テーブルを作成し、 すべての属性が属性が子テーブルのレコードから得られるようにするでしょう。 このアプローチの欠点は、特定の属性値を取得しようとした時に表れます。 すべての関連レコードをループ処理して、見つけたい属性と属性カラムの値を比較し、 それらが一致すれば同じレコードの値カラムから値を取得しなければなりません。
ORM の EAV コンテナは、上と同じ実装をしていますが、エンティティと属性をマージすることができるので、 属性はエンティティのプロパティと同様に扱うことができます。 こうして EAV の実装に必要とされる、多数のカラムが擬似的に実現されます。
実装
モデルで EAV コンテナを有効にするには、属性と値のカラムを持った子テーブルがモデルに対して必要になります。 子テーブルは「1 対多 (One-to-Many) 」か「多対多 (Many-to-Many) 」の関係でなければなりません。
それでは例題として、病院の患者と、その病院が保持している患者の統計情報のモデルを作ってみましょう。 それは患者の病気に依存していて、前もって予測することはできません。
class Model_Patient extends \Orm\Model
{
// このモデルのプロパティのリスト、この例では最短で
protected static $_properties = array(
'id', // 主キー
);
// 通常の方法で statistics との関係を記述します
protected static $_has_many = array(
'statistics' => array(
'key_from' => 'id', // このモデルのキー
'model_to' => 'Model_Statistic', // 関連するモデル
'key_to' => 'patient_id', // 関連するモデルでのキー
'cascade_save' => true, // 関係するテーブルが保存されるときに同時にアップデート
'cascade_delete' => true, // 親テーブルの関連レコードが削除されるときに同時に削除
)
);
// EAV コンテナはこのように定義します
protected static $_eav = array(
'statistics' => array( // EAV データを格納する statistics の関係を利用
'attribute' => 'key', //関連するテーブル内のキー列は属性を含む
'value' => 'value', // 関連するテーブル内の値列は値を含む
)
);
}
class Model_Statistic extend \Orm\Model
{
// このモデルのプロパティのリスト
protected static $_properties = array(
'id', // 主キー
'patient_id', // 外部キー
'key', // 属性カラム
'value', // 値カラム
);
// patient の関係は通常通りの方法で設定
protected static $_belongs_to = array(
'patient' => array(
'key_from' => 'patient_id',
'model_to' => 'Model_Patient',
'key_to' => 'id',
'cascade_save' => true,
'cascade_delete' => true,
)
);
}
それぞれ違うテーブルにリンクする複数の EAV を定義することもできます。 その場合、属性が一致するものが見つかるまで、定義された順序ですべてのコンテナを検索します。 もしプロパティがどこにも定義されていない場合は、 モデルに対する通常のプロパティと同様な処理をします。つまり例外が投げられるということです。
例
上記の設定を利用して、次のようなデータセットを用意してみましょう:
// SELECT * FROM patient
+----+-----------+
| id | name |
+----+-----------+
| 1 | MisterIll |
| 2 | MissIll |
+----+-----------+
// SELECT * FROM statistics
+----+------------+---------------+----------------+
| id | patient_id | key | value |
+----+------------+---------------+ ---------------+
| 1 | 1 | Temperature | 38.4 |
| 2 | 1 | Coughing | yes |
| 3 | 1 | Headache | no |
| 4 | 2 | Temperature | 37.9 |
| 5 | 2 | Heartbeat | 98 |
+----+------------+---------------+----------------+
これらのデータは、以下のようにしてアクセスすることができます:
// いくつかのオブジェクトを作ります
$mr = Patient::find(1);
$ms = Patient::find(2);
// これで属性を直接得ることができるようになりました
echo $mr->Temperature; // '38.4'
echo $ms->Temperature; // '37.9'
echo $mr->Headache; // 'yes'
echo $ms->Headache; // プロパティが見つからないので例外が投げられます
//または、それらを直接設定
$mr->Temperature = '36.9'; // 私たちの患者は改善しているようだ
$mr->save(); // 患者記録を更新し、EAV レコードも更新
// 古いやり方でアクセスすることもできます
foreach ($mr->statistics as $statistic)
{
// それぞれの統計情報に対して何かをしたいときはここに書きます
}
ここで留意すべきは、利用しているモデルが定義済みの EAV コンテナを有する場合、すべての新しいプロパティは新しい EAV キーとして見られるために、もはや、そのモデルのカスタムデータプロパティを利用することはできないと言うことである。