EAVコンテナ

「エンティティ-アトリビュート-バリュー (Entity-attribute-value) モデル (EAV) は、潜在的には大変に多くの属性(プロパティやパラメータ)を記述できることが必要だが、 通常は特定のエンティティに適用される数はさほど多くない場合に適したデータモデルです。 数学ではこのモデルは疎行列として知られています。 EAVはまた、オブジェクト-属性-値モデル、垂直データベースモデル、オープンスキーマなどと呼ばれることもあります。」

出展: ウィキペディア(英語).

はじめに

ほとんどの人は、上記の最初の段落で迷子になるので、例を上げてこれを説明して行きましょう。

エンティティ(レコードを表すORM用語)が子レコードに対して関連するたくさんの属性を持つ時には、通常 EAV のコンテナを使用します。 しかし全レコードを見ると、それらの属性はレコードごとに異なっているかも知れません。 これは属性名をそのままカラム名にしたエンティティテーブルを作ることは不可能であることを意味しています。 なぜならあまりにも多く存在するだろうカラムのうちほとんどはデータを持つことはないからです。 また列を事前に定義する必要が有るため、動的属性で対処することもできません。

この問題を「リレーショナル」スタイルで解決するために、「一対多(One-to-Many)」の関係でエンティティに対する子テーブルを作成し、 すべての属性が属性が子テーブルのレコードから得られるようにするでしょう。 このアプローチの欠点は、特定の属性値を取得しようとした時に表れます。 すべての関連レコードをループ処理して、見つけたい属性と属性カラムの値を比較し、 それらが一致すれば同じレコードの値カラムから値を取得しなければなりません。

ORM の EAV コンテナは、上と同じ実装をしていますが、エンティティと属性をマージすることができるので、 属性はエンティティのプロパティと同様に扱うことができます。 こうして EAV の実装に必要とされる、多数のカラムが擬似的に実現されます。

実装

モデルで EAV コンテナを有効にするには、属性と値のカラムを持った子テーブルがモデルに対して必要になります。 子テーブルは「一対多(One-to-Many)」か「多対多 (Many-to-Many)」の関係でなければなりません。

それでは例題として、病院の患者と、その病院が保持している患者の統計情報のモデルを作ってみましょう。 それは患者の病気に依存していて、前もって予測することはできません。

class Patient extends \Orm\Model
{
	// このモデルのプロパティのリスト、この例では最短で
	protected static $_properties = array(
		'id',				// primary key
	);

	// 通常の方法で statistics との関係を記述します
	protected static $_has_many = array(
		'statistics' => array(
			'key_from' => 'id',		// このモデルのキー
			'key_to' => 'patient_id',	// 関連するモデルでのキー
			'cascade_save' => true,	// 関係するテーブルが保存されるときに同時にアップデート
			'cascade_delete' => true,	// 親テーブルの関連レコードが削除されるときに同時に削除
		)
	);

	// EAV コンテナはこのように定義します
	protected static $_eav = array(
		'statistics' => array(		// we use the statistics relation to store the EAV data
			'attribute' => 'key',	// the key column in the related table contains the attribute
			'value' => 'value',		// the value column in the related table contains the value
		)
	);
}

class Statistics extend \Orm\Model
{
	// このモデルのプロパティのリスト
	protected static $_properties = array(
		'id',				// primary key
		'patient_id',			// foreign key
		'key',				// attribute column
		'value',			// value column
	);
}

それぞれ違うテーブルにリンクする複数の 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();			// patient レコードと EAV レコードをアップデートします

// 古いやり方でアクセスすることもできます
foreach ($mr->statistics as $statistic)
{
	// それぞれの統計情報に対して何かをしたいときはここに書きます
}

現時点では、関連テーブルのEAVコンテナに新しい属性を追加する時にこのアクセスメソッドは使えません。 その場合は古いやり方を使って下さい。 つまりこのようにします。 $mr->statistics[] = Statistics::forge($newdata);