“啊,我是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的EventSubscriberuse 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属性mappingif ($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])) {//如果是文本类型需要设置数据库lengthif (method_exists($fieldDepartConfiguration, 'getLength')) {$fieldMapping['length'] = $fieldDepartConfiguration->getLength(); } }if (in_array($fieldEntityClassName, [DecimalItem::class])) {//如果是小数类型则需要添加precision,scaleif ($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!