本文目录导读:

在 PHP 中,“实体属性值模型”(Entity-Attribute-Value,简称 EAV)是一种数据模型设计模式,以下我将详细介绍它的概念、实现方式和使用方法。
EAV 模型是什么?
EAV 模型通过三张表来存储灵活的数据结构:
- 实体表(Entity):存储主要对象
- 属性表(Attribute):定义可用的属性
- 值表(Value):存储具体的属性值
这种设计允许在不修改表结构的情况下,动态地为实体添加新属性。
数据库结构设计
-- 实体表
CREATE TABLE entities (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
created_at TIMESTAMP
);
-- 属性定义表
CREATE TABLE attributes (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) UNIQUE, -- 属性名称
type VARCHAR(20) -- 属性类型: string, int, float, datetime等
);
-- 属性值表
CREATE TABLE attribute_values (
id INT PRIMARY KEY AUTO_INCREMENT,
entity_id INT,
attribute_id INT,
value_text TEXT, -- 文本类型值
value_int INT, -- 整数类型值
value_float FLOAT, -- 浮点数类型值
value_datetime DATETIME, -- 日期时间类型值
FOREIGN KEY (entity_id) REFERENCES entities(id),
FOREIGN KEY (attribute_id) REFERENCES attributes(id)
);
PHP 实现类
基础 EAV 实体类
<?php
class EAVEntity {
protected $id;
protected $name;
protected $attributes = []; // 存储属性名 => 值的关联数组
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
// 动态设置属性
public function setAttribute($name, $value) {
$this->attributes[$name] = $value;
return $this;
}
// 动态获取属性
public function getAttribute($name) {
return isset($this->attributes[$name])
? $this->attributes[$name]
: null;
}
// 获取所有属性
public function getAllAttributes() {
return $this->attributes;
}
// 保存实体和属性
public function save() {
// 保存实体基本信息
$stmt = $this->db->prepare(
"INSERT INTO entities (name, created_at) VALUES (?, NOW())"
);
$stmt->execute([$this->name]);
$this->id = $this->db->lastInsertId();
// 保存所有属性值
foreach ($this->attributes as $name => $value) {
if ($value === null) continue;
// 获取或创建属性
$attrId = $this->getOrCreateAttribute($name);
// 根据类型存储值
$type = gettype($value);
$this->saveValue($attrId, $value, $type);
}
return $this->id;
}
// 加载实体及其属性
public function load($id) {
// 加载实体信息
$stmt = $this->db->prepare("SELECT * FROM entities WHERE id = ?");
$stmt->execute([$id]);
$entityData = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$entityData) return false;
$this->id = $entityData['id'];
$this->name = $entityData['name'];
// 加载属性值
$sql = "SELECT a.name, v.value_text, v.value_int,
v.value_float, v.value_datetime
FROM attribute_values v
JOIN attributes a ON v.attribute_id = a.id
WHERE v.entity_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$id]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$this->attributes[$row['name']] = $this->extractValue($row);
}
return true;
}
// 辅助方法
protected function getOrCreateAttribute($name) {
$stmt = $this->db->prepare("SELECT id FROM attributes WHERE name = ?");
$stmt->execute([$name]);
$result = $stmt->fetch();
if ($result) {
return $result['id'];
}
// 创建新属性
$stmt = $this->db->prepare("INSERT INTO attributes (name, type) VALUES (?, ?)");
$stmt->execute([$name, gettype($this->attributes[$name])]);
return $this->db->lastInsertId();
}
protected function saveValue($attrId, $value, $type) {
$values = [
'entity_id' => $this->id,
'attribute_id' => $attrId
];
switch ($type) {
case 'integer':
$values['value_int'] = $value;
break;
case 'double':
$values['value_float'] = $value;
break;
case 'boolean':
$values['value_int'] = $value ? 1 : 0;
break;
default:
$values['value_text'] = (string)$value;
}
$sql = "INSERT INTO attribute_values
(entity_id, attribute_id, value_text, value_int, value_float)
VALUES (:entity_id, :attribute_id, :value_text, :value_int, :value_float)";
$stmt = $this->db->prepare($sql);
$stmt->execute($values);
}
protected function extractValue($row) {
// 根据存储位置提取实际值
if ($row['value_int'] !== null) {
return (int)$row['value_int'];
}
if ($row['value_float'] !== null) {
return (float)$row['value_float'];
}
if ($row['value_datetime'] !== null) {
return new DateTime($row['value_datetime']);
}
return $row['value_text'];
}
// Getter/Setter
public function getId() { return $this->id; }
public function getName() { return $this->name; }
public function setName($name) {
$this->name = $name;
return $this;
}
}
使用示例
<?php
// 初始化数据库连接
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 创建一个产品
$product = new EAVEntity($pdo);
$product->setName('智能手机')
->setAttribute('color', '黑色')
->setAttribute('price', 2999.99)
->setAttribute('stock', 100)
->setAttribute('warranty_years', 2)
->save();
echo "Product ID: " . $product->getId() . "<br>";
// 加载产品并访问属性
$loadedProduct = new EAVEntity($pdo);
if ($loadedProduct->load($product->getId())) {
echo "名称: " . $loadedProduct->getName() . "<br>";
echo "颜色: " . $loadedProduct->getAttribute('color') . "<br>";
echo "价格: " . $loadedProduct->getAttribute('price') . "<br>";
echo "库存: " . $loadedProduct->getAttribute('stock') . "<br>";
}
高级用法 - 使用查询模型
<?php
class EAVQueryBuilder {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
// 根据属性条件查询实体
public function findEntitiesByAttribute($attrName, $value, $operator = '=') {
$sql = "SELECT DISTINCT e.*
FROM entities e
JOIN attribute_values v ON e.id = v.entity_id
JOIN attributes a ON v.attribute_id = a.id
WHERE a.name = ?
AND (" . $this->getValueColumn($value) . " $operator ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$attrName, $value]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取具有特定属性的所有实体
public function findEntitiesWithAttribute($attrName) {
$sql = "SELECT DISTINCT e.*
FROM entities e
JOIN attribute_values v ON e.id = v.entity_id
JOIN attributes a ON v.attribute_id = a.id
WHERE a.name = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$attrName]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
private function getValueColumn($value) {
if (is_int($value)) return 'v.value_int';
if (is_float($value)) return 'v.value_float';
return 'v.value_text';
}
}
// 使用查询模型
$queryBuilder = new EAVQueryBuilder($pdo);
// 查找价格大于2000的产品
$expensiveProducts = $queryBuilder->findEntitiesByAttribute('price', 2000, '>');
print_r($expensiveProducts);
// 查找所有有颜色属性的实体
$coloredEntities = $queryBuilder->findEntitiesWithAttribute('color');
print_r($coloredEntities);
EAV 模型的优缺点
优点
- ✅ 高度灵活:无需修改表结构即可添加新属性
- ✅ 扩展性好:适合存储异构数据
- ✅ 节省空间:稀疏数据不会占用大量存储空间
缺点
- ❌ 查询复杂:需要多次 JOIN
- ❌ 性能问题:数据量大时查询效率低
- ❌ 数据完整性:难以实施数据库级完整性约束
实际应用场景
- 产品目录系统:不同类别产品有不同属性
- 配置系统:可动态增删配置项
- CMS系统类型有不同字段
- 用户自定义字段:让用户自定义信息项
建议
如果项目的数据结构相对稳定,建议使用传统的字段方式;只有在需要高度灵活性时才考虑 EAV 模型,对于大规模应用,可以考虑使用 NoSQL 数据库(如 MongoDB)作为替代方案。