开发TEEBB时踩过的坑--Doctrine动态Mapping
摘要:TEEBB(至文内容管理系统)是基于Symfony开发的,使用了Doctrine的ORM套件。 TEEBB中类型EntityType的每个字段都在Mysql中对应了一张表。
“啊,我是cgx,我现在在LA,我遇到了一班很坏很坏的人呐,VX转账300帮我回HK。”
不知道为什么写这篇时脑子里就冒出了这个梗。“You Know” TEEBB(至文内容管理系统)是基于Symfony开发的,使用了Doctrine的ORM套件。
TEEBB中类型EntityType的每个字段都在Mysql中对应了一张表。看下图:
我们在“文章”类型中添加了“图像”字段,对应的会在Mysql中动态的添加一张表。 图像字段表Entity如下:
//\Teebb\CoreBundle\Entity\Fields\ReferenceImageItem 注:因篇幅原因省去getters 和 setters
/**
* 引用图像字段在库中的存储
* @ORM\Entity(repositoryClass="Teebb\CoreBundle\Repository\Fields\FieldRepository")
*
* @author Quan Weiwei <qww.zone@gmail.com>
*/
class ReferenceImageItem extends BaseFieldItem
{
/**
* 单向多对一关系 对应文件库的entity_id
* @var FileManaged|null
* @ORM\ManyToOne(targetEntity="Teebb\CoreBundle\Entity\FileManaged")
* @ORM\JoinColumn(name="reference_file_id")
*/
private $value;
/**
* 图像alt信息
* @var string|null
* @Gedmo\Translatable
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $alt;
/**
* 图像title信息
* @var string|null
* @Gedmo\Translatable
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $title;
/**
* 图像宽度信息
* @var integer
* @ORM\Column(type="integer", nullable=true)
*/
private $width;
/**
* 图像高度信息
* @var integer
* @ORM\Column(type="integer", nullable=true)
*/
private $height;
}
BaseFieldItem类如下:
/**
* Field Entity基类
*
* @ORM\MappedSuperclass()
*
* @author Quan Weiwei <qww.zone@gmail.com>
*/
class BaseFieldItem
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
protected $id;
/**
* 内容实体类型的别名 比如:'content', 'taxonomy', 'comment'等
* @var string
* @ORM\Column(type="string", length=255, nullable=false)
*/
protected $types;
/**
* 类型实体entity, many-to-one, 多个字段值对应一个entity
* @var object|null
*/
protected $entity;
/**
* 同一字段不限制数量时,用于排序
* @var integer
* @ORM\Column(type="integer")
*/
protected $delta;
}
BaseFieldItem的entity属性对应的是类型EntityType的实体Entity,字段对象使用此属性关联到具体的实体Entity对象。如果内容类型添加了图像字段entity属性关联到Content类的对象,如果是分类类型添加了图像字段则entity属性需要关联到Taxonomy类的对象。所以entity属性就需要动态mapping。
Doctrine的事件中提供了loadClassMetadata事件,可以通过此事件对字段属性进行动态mapping。
编写我们的Subscriber对此事件进行订阅:
//\Teebb\CoreBundle\Subscriber\DynamicChangeFieldMetadataSubscriber
//注:篇幅原因此类并非完整,请下载源码查看完整类实现
//此类实现的是Doctrine的EventSubscriber
use Doctrine\Common\EventSubscriber;
class DynamicChangeFieldMetadataSubscriber implements EventSubscriber
{
/**
* @param LoadClassMetadataEventArgs $args
* @throws MappingException
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
$className = $classMetadata->getName();
//如果 $className 是以下类型则进行动态字段映射修改
$modifyEntityIemArray = [
BooleanItem::class,
DatetimeItem::class,
DecimalItem::class,
EmailItem::class,
FloatItem::class,
IntegerItem::class,
LinkItem::class,
ListFloatItem::class,
ListIntegerItem::class,
ReferenceContentItem::class,
ReferenceFileItem::class,
ReferenceImageItem::class,
ReferenceTaxonomyItem::class,
ReferenceUserItem::class,
StringFormatItem::class,
StringItem::class,
TextFormatItem::class,
TextFormatSummaryItem::class,
TextItem::class,
CommentItem::class
];
if (in_array($className, $modifyEntityIemArray)) {
$this->modifyFieldEntityClassMetaData($classMetadata, $this->getFieldConfiguration(), $this->getTargetContentClassName());
}
}
/**
* 动态修改ClassMetadata
* @param ClassMetadata $classMetadata
* @param FieldConfiguration|null $fieldConfiguration
* @param string|null $entityClassName content entity全类名
* @throws MappingException
*/
private function modifyFieldEntityClassMetaData(ClassMetadata $classMetadata, ?FieldConfiguration $fieldConfiguration, ?string $entityClassName)
{
if ($fieldConfiguration == null || $entityClassName == null) {
return;
}
//设置字段表名
$fieldAlias = $fieldConfiguration->getFieldAlias();
$classMetadata->setPrimaryTable(['name' => $fieldConfiguration->getBundle() . '__field_' . $fieldAlias]);
/**@var FieldDepartConfigurationInterface $fieldDepartConfiguration * */
$fieldDepartConfiguration = $fieldConfiguration->getSettings();
$doctrineType = $fieldDepartConfiguration->getType();
//映射字段的entity属性
if (!$classMetadata->hasAssociation('entity')) {
$classMetadata->mapManyToOne([
'fieldName' => 'entity',
'targetEntity' => $entityClassName,
'cascade' => ['remove', 'persist'],
'joinColumns' => [
[
'name' => 'entity_id',
'referencedColumnName' => 'id',
'nullable' => false,
]
]
]);
}
//处理引用内容、分类、用户类型的value属性mapping
if ($doctrineType === 'entity' && !$classMetadata->hasAssociation('value')) {
if (!method_exists($fieldDepartConfiguration, 'getReferenceTargetEntity')) {
throw new \RuntimeException(sprintf('Reference field configuration "%s" must define "getReferenceTargetEntity" method.', get_class($fieldDepartConfiguration)));
}
$classMetadata->mapManyToOne([
'fieldName' => 'value',
'targetEntity' => $fieldDepartConfiguration->getReferenceTargetEntity(),
'cascade' => ['remove', 'persist'],
'joinColumns' => [
[
'name' => 'reference_entity_id',
'referencedColumnName' => 'id',
'nullable' => true,
]
]
]);
}
//对于非引用类型value属性的映射
if ($doctrineType !== 'entity' && !$classMetadata->hasField('value')) {
//动态Mapping字段value,
$fieldMapping = array(
'fieldName' => 'value',
'columnName' => 'field_value',
'type' => $doctrineType,
'nullable' => true
);
//字段的Entity全类名
$fieldEntityClassName = $classMetadata->getName();
if (in_array($fieldEntityClassName, [StringFormatItem::class, StringItem::class])) {
//如果是文本类型需要设置数据库length
if (method_exists($fieldDepartConfiguration, 'getLength')) {
$fieldMapping['length'] = $fieldDepartConfiguration->getLength();
}
}
if (in_array($fieldEntityClassName, [DecimalItem::class])) {
//如果是小数类型则需要添加precision,scale
if ($doctrineType == 'decimal') {
$fieldMapping['precision'] = $fieldDepartConfiguration->getPrecision();
$fieldMapping['scale'] = $fieldDepartConfiguration->getScale();
}
}
$classMetadata->mapField($fieldMapping);
}
}
public function getSubscribedEvents(): array
{
return [
Events::loadClassMetadata,
];
}
}
我们还需要在Symfony中对此类进行些许配置:
<service id="teebb.core.event.dynamic_field_mapping_subscriber" public="true"
class="Teebb\CoreBundle\Subscriber\DynamicChangeFieldMetadataSubscriber">
<!--此处需要使用doctrine.event_subscriber tag 名称-->
<tag name="doctrine.event_subscriber"/>
</service>
当ORM对数据进行存取时会自动dispatch loadClassMetadata
事件,我们在需要动态Mapping时对Container中的teebb.core.event.dynamic_field_mapping_subscriber
进行动态修改即可,如下:
//\Teebb\CoreBundle\Controller\SubstanceDBALOptionsTrait
$dynamicFieldMappingSubscriber = $container->get('teebb.core.event.dynamic_field_mapping_subscriber');
//动态修改字段entity的mapping
$dynamicFieldMappingSubscriber->setFieldConfiguration($field);
$dynamicFieldMappingSubscriber->setTargetContentClassName($contentClassName);
这样就可以实现ORM的动态Mapping。
特别提醒:
当我们转为生产环境并添加字段时,会发现动态Mapping并没有生效!
prod.log日志报的错误是这样的:
request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\Exception\InvalidFieldNameException: "An exception occurred while executing 'SELECT * FROM content__field_ri_qi WHERE (entity_id = ?) AND (types = ?) ORDER BY delta ASC' with params [1, "article"]: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'entity_id' in 'where clause'" at /Users/quanweiwei/Repository/php/teebbstudios/teebb/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php line 79 {"exception":"[object] (Doctrine\\DBAL\\Exception\\InvalidFieldNameException(code: 0): An exception occurred while executing 'SELECT * FROM content__field_ri_qi WHERE (entity_id = ?) AND (types = ?) ORDER BY delta ASC' with params [1, \"article\"]:\n\nSQLSTATE[42S22]: Column not found: 1054 Unknown column 'entity_id' in 'where clause' at /Users/quanweiwei/Repository/php/teebbstudios/teebb/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php:79)\n[previous exception] [object] (Doctrine\\DBAL\\Driver\\PDO\\Exception(code: 42S22): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'entity_id' in 'where clause' at /Users/quanweiwei/Repository/php/teebbstudios/teebb/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Exception.php:18)\n[previous exception] [object] (PDOException(code: 42S22): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'entity_id' in 'where clause' at /Users/quanweiwei/Repository/php/teebbstudios/teebb/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:115)"} []
日志提示没有找到entity_id列,我们明明已经动态mapping了,为什么? 原因是这样的:
当Doctrine从dev环境转入prod环境,第一次生成缓存时,会把所有Entity的注解都读取并生成Proxy类,以此提升性能,我们的字段Entity也进行了缓存,当对数据进行存取时会使用缓存中的Proxy类而不使用动态mapping。如下图:
通过修改doctrine.yaml对auto_generate_proxy_classes的设置可以修复此问题:
doctrine:
orm:
auto_generate_proxy_classes: EVAL #使用EVAL不用将生成的proxy类存入本地磁盘
修改之后请记得删除缓存!!!
硬核的删除缓存的方法: 项目var目录下,删除所有东西!!!
特别提醒:
如果是一般开发请设计好Entity类,尽量避免使用动态Mapping!!!
OK,至此这个坑算是踩过去了。但是你知道的ORM开发上很方便,但是性能上很呵呵。
在TEEBB的1.x版本我将会用更好的办法提升性能。PEACE!