Doctrine ORM 高级映射技巧实战讲座
大家好,今天我们来深入探讨 Doctrine ORM 的几个高级映射技巧:继承映射、嵌入式对象和自定义数据类型。这些技巧能够帮助我们更好地组织数据模型,提高代码复用性,并满足特定的数据存储需求。
一、继承映射 (Inheritance Mapping)
继承映射允许我们将面向对象编程中的继承关系映射到数据库表结构中。 Doctrine ORM 提供了三种主要的继承映射策略:单表继承 (Single Table Inheritance)、类表继承 (Class Table Inheritance) 和连接表继承 (Joined Table Inheritance)。
1. 单表继承 (Single Table Inheritance)
单表继承将整个继承层级结构映射到单个数据库表中。 这意味着所有子类和父类的属性都存储在同一个表中,并使用一个鉴别器列 (discriminator column) 来区分不同的子类。
示例:
假设我们有一个 Vehicle 父类,以及 Car 和 Truck 两个子类。
实体类定义:
<?php
use DoctrineORMMapping as ORM;
/**
* @ORMEntity
* @ORMInheritanceType("SINGLE_TABLE")
* @ORMDiscriminatorColumn(name="discr", type="string")
* @ORMDiscriminatorMap({"vehicle" = "Vehicle", "car" = "Car", "truck" = "Truck"})
*/
class Vehicle
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
protected $id;
/**
* @ORMColumn(type="string")
*/
protected $model;
public function getId() { return $this->id; }
public function setModel($model) { $this->model = $model; }
public function getModel() { return $this->model; }
}
/**
* @ORMEntity
*/
class Car extends Vehicle
{
/**
* @ORMColumn(type="integer")
*/
private $numberOfDoors;
public function setNumberOfDoors($numberOfDoors) { $this->numberOfDoors = $numberOfDoors; }
public function getNumberOfDoors() { return $this->numberOfDoors; }
}
/**
* @ORMEntity
*/
class Truck extends Vehicle
{
/**
* @ORMColumn(type="float")
*/
private $loadCapacity;
public function setLoadCapacity($loadCapacity) { $this->loadCapacity = $loadCapacity; }
public function getLoadCapacity() { return $this->loadCapacity; }
}
数据库表结构 (vehicle):
| Column | Type | Description |
|---|---|---|
| id | INTEGER | 主键 |
| model | VARCHAR | 车辆型号 |
| discr | VARCHAR | 鉴别器列,用于区分车辆类型 (vehicle, car, truck) |
| numberOfDoors | INTEGER | 轿车车门数量 (仅 Car 类使用) |
| loadCapacity | FLOAT | 卡车载重能力 (仅 Truck 类使用) |
优点:
- 查询简单高效,只需要查询一个表。
- 易于理解和维护。
缺点:
- 可能存在大量的 NULL 值,特别是当子类有很多特有属性时。
- 单个表可能会变得非常庞大。
- 不容易添加新的子类,可能需要修改现有表结构。
2. 类表继承 (Class Table Inheritance)
类表继承为每个类 (包括父类和子类) 创建一个单独的数据库表。 子类的表包含所有从父类继承的属性,以及自身特有的属性。
示例:
实体类定义 (与单表继承相同):
<?php
use DoctrineORMMapping as ORM;
/**
* @ORMEntity
* @ORMInheritanceType("JOINED")
* @ORMDiscriminatorColumn(name="discr", type="string")
* @ORMDiscriminatorMap({"vehicle" = "Vehicle", "car" = "Car", "truck" = "Truck"})
*/
class Vehicle
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
protected $id;
/**
* @ORMColumn(type="string")
*/
protected $model;
public function getId() { return $this->id; }
public function setModel($model) { $this->model = $model; }
public function getModel() { return $this->model; }
}
/**
* @ORMEntity
*/
class Car extends Vehicle
{
/**
* @ORMColumn(type="integer")
*/
private $numberOfDoors;
public function setNumberOfDoors($numberOfDoors) { $this->numberOfDoors = $numberOfDoors; }
public function getNumberOfDoors() { return $this->numberOfDoors; }
}
/**
* @ORMEntity
*/
class Truck extends Vehicle
{
/**
* @ORMColumn(type="float")
*/
private $loadCapacity;
public function setLoadCapacity($loadCapacity) { $this->loadCapacity = $loadCapacity; }
public function getLoadCapacity() { return $this->loadCapacity; }
}
数据库表结构:
-
vehicle:
Column Type Description id INTEGER 主键 model VARCHAR 车辆型号 -
car:
Column Type Description id INTEGER 主键 (外键,关联 vehicle.id) numberOfDoors INTEGER 轿车车门数量 -
truck:
Column Type Description id INTEGER 主键 (外键,关联 vehicle.id) loadCapacity FLOAT 卡车载重能力
优点:
- 每个表只包含相关的属性,避免了大量的 NULL 值。
- 表结构相对简单。
缺点:
- 查询需要进行 JOIN 操作,性能可能较低。
- 数据库表数量较多,管理复杂。
3. 连接表继承 (Joined Table Inheritance)
连接表继承与类表继承类似,也为每个类创建一个单独的数据库表。 然而,子类的表只包含自身特有的属性,以及一个指向父类表的主键外键。 父类的表包含所有公共属性。
示例:
实体类定义 (与单表继承相同):
<?php
use DoctrineORMMapping as ORM;
/**
* @ORMEntity
* @ORMInheritanceType("JOINED")
* @ORMDiscriminatorColumn(name="discr", type="string")
* @ORMDiscriminatorMap({"vehicle" = "Vehicle", "car" = "Car", "truck" = "Truck"})
*/
class Vehicle
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
protected $id;
/**
* @ORMColumn(type="string")
*/
protected $model;
public function getId() { return $this->id; }
public function setModel($model) { $this->model = $model; }
public function getModel() { return $this->model; }
}
/**
* @ORMEntity
*/
class Car extends Vehicle
{
/**
* @ORMColumn(type="integer")
*/
private $numberOfDoors;
public function setNumberOfDoors($numberOfDoors) { $this->numberOfDoors = $numberOfDoors; }
public function getNumberOfDoors() { return $this->numberOfDoors; }
}
/**
* @ORMEntity
*/
class Truck extends Vehicle
{
/**
* @ORMColumn(type="float")
*/
private $loadCapacity;
public function setLoadCapacity($loadCapacity) { $this->loadCapacity = $loadCapacity; }
public function getLoadCapacity() { return $this->loadCapacity; }
}
数据库表结构:
-
vehicle:
Column Type Description id INTEGER 主键 model VARCHAR 车辆型号 -
car:
Column Type Description id INTEGER 主键 (外键,关联 vehicle.id) numberOfDoors INTEGER 轿车车门数量 -
truck:
Column Type Description id INTEGER 主键 (外键,关联 vehicle.id) loadCapacity FLOAT 卡车载重能力
优点:
- 每个表只包含相关的属性,避免了大量的 NULL 值。
- 易于扩展,添加新的子类不需要修改父类表。
缺点:
- 查询需要进行 JOIN 操作,性能可能较低。
- 数据库表数量较多,管理复杂。
选择哪种继承映射策略?
| 特性 | 单表继承 | 类表继承 | 连接表继承 |
|---|---|---|---|
| 查询性能 | 高 | 低 | 低 |
| 存储空间利用率 | 低 | 高 | 高 |
| 易于维护 | 中 | 低 | 低 |
| 扩展性 | 低 | 中 | 高 |
总的来说:
- 如果继承层级结构简单,且对查询性能要求较高,可以选择单表继承。
- 如果继承层级结构复杂,且子类有很多特有属性,可以选择类表继承或连接表继承。
- 如果需要频繁添加新的子类,可以选择连接表继承。
二、嵌入式对象 (Embeddable Objects)
嵌入式对象允许我们将一个实体类嵌入到另一个实体类中。 这可以帮助我们更好地组织数据模型,提高代码复用性,并简化实体类的结构。
示例:
假设我们有一个 Address 类,用于表示地址信息。 我们希望将 Address 类嵌入到 User 类中。
实体类定义:
<?php
use DoctrineORMMapping as ORM;
/**
* @ORMEmbeddable
*/
class Address
{
/**
* @ORMColumn(type="string")
*/
private $street;
/**
* @ORMColumn(type="string")
*/
private $city;
/**
* @ORMColumn(type="string")
*/
private $zipCode;
public function setStreet($street) { $this->street = $street; }
public function getStreet() { return $this->street; }
public function setCity($city) { $this->city = $city; }
public function getCity() { return $this->city; }
public function setZipCode($zipCode) { $this->zipCode = $zipCode; }
public function getZipCode() { return $this->zipCode; }
}
/**
* @ORMEntity
*/
class User
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
private $id;
/**
* @ORMColumn(type="string")
*/
private $name;
/**
* @ORMEmbedded(class="Address", columnPrefix="address_")
*/
private $address;
public function __construct()
{
$this->address = new Address();
}
public function getId() { return $this->id; }
public function setName($name) { $this->name = $name; }
public function getName() { return $this->name; }
public function getAddress() { return $this->address; }
public function setAddress(Address $address) { $this->address = $address; }
}
数据库表结构 (user):
| Column | Type | Description |
|---|---|---|
| id | INTEGER | 主键 |
| name | VARCHAR | 用户名 |
| address_street | VARCHAR | 街道地址 |
| address_city | VARCHAR | 城市 |
| address_zip_code | VARCHAR | 邮政编码 |
@ORMEmbedded(class="Address", columnPrefix="address_") 说明:
class="Address"指定要嵌入的类。columnPrefix="address_"指定嵌入式对象属性在数据库表中的列名前缀。
优点:
- 更好地组织数据模型,将相关的属性封装在一起。
- 提高代码复用性,可以在多个实体类中嵌入同一个嵌入式对象。
- 简化实体类的结构,使实体类更易于理解和维护。
注意事项:
- 嵌入式对象没有自己的 ID。
- 嵌入式对象的值被复制到包含它的实体中。
- 对嵌入式对象的更改只有在包含它的实体被持久化时才会反映到数据库中。
三、自定义数据类型 (Custom Data Types)
Doctrine ORM 提供了许多内置的数据类型,例如 string、integer、datetime 等。 然而,在某些情况下,我们需要存储一些非标准的数据类型,例如 JSON 数据、地理位置数据等。 自定义数据类型允许我们定义自己的数据类型,并将它们映射到数据库中的特定列类型。
示例:
假设我们需要存储 JSON 数据。 我们可以创建一个自定义数据类型 json_array,将它映射到数据库中的 TEXT 或 JSON 列。
自定义数据类型类:
<?php
namespace AppDBALTypes;
use DoctrineDBALPlatformsAbstractPlatform;
use DoctrineDBALTypesType;
class JsonArrayType extends Type
{
const JSON_ARRAY = 'json_array';
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getJsonTypeDeclarationSQL($fieldDeclaration);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return null;
}
$value = (is_resource($value)) ? stream_get_contents($value) : $value;
return json_decode($value, true);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return json_encode($value);
}
public function getName()
{
return self::JSON_ARRAY;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
解释:
getSQLDeclaration()方法返回数据库列的 SQL 声明。convertToPHPValue()方法将数据库中的值转换为 PHP 值。convertToDatabaseValue()方法将 PHP 值转换为数据库中的值。getName()方法返回数据类型的名称。requiresSQLCommentHint()方法返回是否需要 SQL 注释提示。
注册自定义数据类型:
在 doctrine.yaml 文件中注册自定义数据类型:
doctrine:
dbal:
types:
json_array: AppDBALTypesJsonArrayType
在实体类中使用自定义数据类型:
<?php
use DoctrineORMMapping as ORM;
/**
* @ORMEntity
*/
class Product
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
private $id;
/**
* @ORMColumn(type="string")
*/
private $name;
/**
* @ORMColumn(type="json_array", nullable=true)
*/
private $attributes;
public function getId() { return $this->id; }
public function setName($name) { $this->name = $name; }
public function getName() { return $this->name; }
public function setAttributes($attributes) { $this->attributes = $attributes; }
public function getAttributes() { return $this->attributes; }
}
数据库表结构 (product):
| Column | Type | Description |
|---|---|---|
| id | INTEGER | 主键 |
| name | VARCHAR | 产品名称 |
| attributes | TEXT/JSON | JSON 数据 |
优点:
- 可以存储非标准的数据类型。
- 可以对数据进行自定义的转换和验证。
- 提高数据模型的灵活性。
注意事项:
- 需要熟悉 Doctrine DBAL 的 API。
- 需要根据不同的数据库平台编写不同的 SQL 声明。
四、实践案例:电商平台商品数据建模
让我们结合上述三种技巧,为一个简单的电商平台商品数据建模。 我们需要存储商品的基本信息、商品属性以及商品的变体(例如不同颜色和尺寸的商品)。
-
商品类型 (继承映射): 商品可以分为
PhysicalProduct(实物商品) 和DigitalProduct(虚拟商品)。 使用单表继承,方便查询所有商品。 -
商品属性 (嵌入式对象): 商品可能有很多属性,例如颜色、尺寸、材质等。 创建一个
ProductAttribute类,用于表示单个属性的键值对,并使用嵌入式对象将属性列表嵌入到Product类中。或者,商品本身有部分信息可以提取出来组成一个Embeddable,比如ProductInfo包含description,specifications,careInstructions。 -
JSON 格式的商品额外信息(自定义数据类型): 某些商品可能需要存储一些额外的、非结构化的信息,例如用户评价、推荐搭配等。 使用自定义数据类型
json_array将这些信息存储在 JSON 格式的列中。
实体类定义:
<?php
use DoctrineORMMapping as ORM;
/**
* @ORMEntity
* @ORMInheritanceType("SINGLE_TABLE")
* @ORMDiscriminatorColumn(name="discr", type="string")
* @ORMDiscriminatorMap({"product" = "Product", "physical" = "PhysicalProduct", "digital" = "DigitalProduct"})
*/
class Product
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
protected $id;
/**
* @ORMColumn(type="string")
*/
protected $name;
/**
* @ORMColumn(type="float")
*/
protected $price;
/**
* @ORMColumn(type="json_array", nullable=true)
*/
protected $extraInfo;
public function getId() { return $this->id; }
public function setName($name) { $this->name = $name; }
public function getName() { return $this->name; }
public function setPrice($price) { $this->price = $price; }
public function getPrice() { return $this->price; }
public function setExtraInfo($extraInfo) { $this->extraInfo = $extraInfo; }
public function getExtraInfo() { return $this->extraInfo; }
}
/**
* @ORMEntity
*/
class PhysicalProduct extends Product
{
/**
* @ORMColumn(type="float")
*/
private $weight;
public function setWeight($weight) { $this->weight = $weight; }
public function getWeight() { return $this->weight; }
}
/**
* @ORMEntity
*/
class DigitalProduct extends Product
{
/**
* @ORMColumn(type="string")
*/
private $downloadLink;
public function setDownloadLink($downloadLink) { $this->downloadLink = $downloadLink; }
public function getDownloadLink() { return $this->downloadLink; }
}
/**
* @ORMEmbeddable
*/
class ProductAttribute
{
/**
* @ORMColumn(type="string")
*/
private $name;
/**
* @ORMColumn(type="string")
*/
private $value;
public function setName($name) { $this->name = $name; }
public function getName() { return $this->name; }
public function setValue($value) { $this->value = $value; }
public function getValue() { return $this->value; }
}
/**
* @ORMEntity
*/
class ProductVariant
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
*/
private $id;
/**
* @ORMManyToOne(targetEntity="Product")
* @ORMJoinColumn(name="product_id", referencedColumnName="id")
*/
private $product;
/**
* @ORMEmbedded(class="ProductAttribute", columnPrefix="attribute_")
*/
private $attribute;
public function __construct()
{
$this->attribute = new ProductAttribute();
}
public function getId() { return $this->id; }
public function setProduct(Product $product) { $this->product = $product; }
public function getProduct() { return $this->product; }
public function setAttribute(ProductAttribute $attribute) { $this->attribute = $attribute; }
public function getAttribute() { return $this->attribute; }
}
数据库表结构 (product):
| Column | Type | Description |
|---|---|---|
| id | INTEGER | 主键 |
| name | VARCHAR | 商品名称 |
| price | FLOAT | 商品价格 |
| discr | VARCHAR | 鉴别器列 (product, physical, digital) |
| weight | FLOAT | 实物商品重量 (仅 PhysicalProduct 类使用) |
| downloadLink | VARCHAR | 虚拟商品下载链接 (仅 DigitalProduct 类使用) |
| extraInfo | TEXT/JSON | JSON 格式的额外信息 |
数据库表结构 (product_variant):
| Column | Type | Description |
|---|---|---|
| id | INTEGER | 主键 |
| product_id | INTEGER | 外键,关联 product.id |
| attribute_name | VARCHAR | 属性名称 |
| attribute_value | VARCHAR | 属性值 |
通过这个实践案例,我们可以看到如何灵活运用继承映射、嵌入式对象和自定义数据类型来构建复杂的数据模型。
更好地组织数据,利用继承提高代码复用
我们学习了 Doctrine ORM 的三种高级映射技巧:继承映射、嵌入式对象和自定义数据类型。 继承映射允许我们映射面向对象编程中的继承关系,嵌入式对象允许我们封装和复用实体类,自定义数据类型允许我们存储非标准的数据类型。通过灵活运用这些技巧,我们可以更好地组织数据模型,提高代码复用性,并满足特定的数据存储需求。