“啊,我是cgx,我现在在LA,我遇到了一班很坏很坏的人呐,VX转账300帮我回HK。”

不知道为什么写这篇时脑子里就冒出了这个梗。“You Know” TEEBB(至文内容管理系统)是基于Symfony开发的,使用了Doctrine的ORM套件。
TEEBB中类型EntityType的每个字段都在Mysql中对应了一张表。看下图:开发TEEBB时踩过的坑--Doctrine动态Mapping_Mapping我们在“文章”类型中添加了“图像”字段,对应的会在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。如下图:

开发TEEBB时踩过的坑--Doctrine动态Mapping_Mapping_02

通过修改doctrine.yaml对auto_generate_proxy_classes的设置可以修复此问题:

doctrine:orm:auto_generate_proxy_classes: EVAL #使用EVAL不用将生成的proxy类存入本地磁盘复制代码

修改之后请记得删除缓存!!!

硬核的删除缓存的方法: 项目var目录下,删除所有东西!!!

特别提醒:
如果是一般开发请设计好Entity类,尽量避免使用动态Mapping!!!复制代码

OK,至此这个坑算是踩过去了。但是你知道的ORM开发上很方便,但是性能上很呵呵,在TEEBB的1.x版本我将会用更好的办法提升性能。PEACE!