EAV 容器

「實體-屬性-值模型(EAV)是一種實體資料模型,描述該實體屬性(特性、參数)的数量可能十分巨大, 但實際上会被應用在所給實體的数量相對適中。 在数學上,这个模型被稱为稀疏矩陣。EAV 也被稱为物件-屬性-值模型, 直式的資料庫模型以及開放的架構。」

來源:Wikipedia (English)

简介

大多数人已经在第一段迷失,所以让我們试著用一个範例來解釋。

當你有一个實體(紀錄在 ORM 中的術語)而它有關聯的屬性做为它的子代時,你通常会使用一个 EAV 容器。 但是,對每一條紀錄來說,这些屬性可以是不同的。 这使得它不可能在實體的資料表中定義这些屬性为行,因为会有太多, 他們大多不会有資料,而且你完全不能处理动態屬性 (因为行需要預先被定義)。

要解決这个「關聯」風格的問題,你会建立一張子表,并使用一對多關係關聯到「實體」的資料表, 其中每个屬性会成为子表中的一筆紀錄。 然而,这種作法的缺點是,为了能夠取得一个指定屬性值,你将不得不循环所有關聯紀錄, 比較屬性行與你尋找的值,如果找到一个符合, 取得該值行的內容。

ORM EAV 容器使用这个相同的實现,但允許你合併實體的屬性, 如此該屬性成为實體紀錄的特性, 從而模擬一个 EAV 實现所需的可變行数。

實现

要在一个模型啟用 EAV 容器,該模型需要一張有屬性及值行的子表, 它需要被定義为一對多或多對多關聯。

让我們建立一个醫院病人的範例,該醫院保留病人的紀錄统計。 这取決於病人的病情,他們不能被預先預测。

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(			// 我們使用 statistics 關聯來儲存 EAV 資料
			'model_to' => 'Model_Statistic',	// 關聯模型
			'attribute' => 'key',		// 在關聯資料表中包含屬性的鍵行
			'value' => 'value',			// 在關聯資料表中包含值的值行
		)
	);
}

class Model_Statistic extend \Orm\Model
{
	// 此模型的特性列表
	protected static $_properties = array(
		'id',					// 主鍵
		'patient_id',				// 外鍵
		'key',					// 屬性行
		'value',				// 值行
	);

	// 設置病人關聯的通常方法
	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 鍵。