首页 > 解决方案 > 为什么 Doctrine 试图通过关系插入新实体?

问题描述

我拥有以下 Doctrine 实体EntityA

<?php

class EntityA
{
    /**
     * @ORM\ManyToOne(targetEntity="EntityB" , inversedBy="propertyA")
     */
    private $propertyB;

    // ....

    public function setEntityB(EntityB $entBRef)
    {
        $this->propertyB = $entBRef;
    }
}

class EntityB
{
    /**
     * @ORM\OneToMany(targetEntity="EntityA", mappedBy="propertyB")
     */
    private $propertyA;
    // ....
}

我也有一RepositoryB堂课:

<?php 

class RepositoryB extends \Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository
{
    public function __construct(\Doctrine\Common\Persistence\ManagerRegistry $registry)
    {
        parent::__construct($registry, EntityB::class);
    }
}

然后在我的课堂上,我试图坚持一个EntityA对象:

<?php

class SomeService
{
    /** @var RepositoryB */
    private $repositoryB;

    /** @var EntityManagerInterface */
    private $entityManager;

    public function __construct(RepositoryB $repositoryB, EntityManagerInterface $entityManager)
    {
        $this->repositoryB = $repositoryB;
        $this->entityManager = $entityManager;
    }

    public function testSomething()
    {
        $entBRef = $this->repositoryB->find(1);

        $newEnt = (new EntityA());
        $newEnt->setEntityB($entBRef);
        $this->entityManager->persist($newEnt);
        $this->flush();
    }
}

但我有以下错误:

通过关系“EntityA#propertyB”找到了一个新实体,该关系未配置为实体的级联持久操作:EntityB。要解决此问题:在此未知实体上显式调用 EntityManager#persist() 或配置级联

这是我的doctrine.yaml文件的样子:

parameters:
    env(DATABASE_URL_WRITE): ''

services:
    gedmo.listener.timestampable:
        class: Org\CompBundle\Gedmo\Timestampable\TimestampableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    gedmo.listener.loggable:
        class: Gedmo\Loggable\LoggableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ "@annotation_reader" ] ]

    gedmo.listener.deleteable:
        class: Gedmo\SoftDeleteable\SoftDeleteableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ '@annotation_reader' ] ]

doctrine:
    dbal:
        default_connection: default
        types:
            microseconds: Org\CompBundle\DBAL\Types\DateTimeMicrosecondsType
        connections:
            read_only:
                url: '%env(resolve:DATABASE_URL_READ)%'
                driver: 'pdo_mysql'
                server_version: '5.7'
                charset: utf8mb4
                default_table_options:
                    charset: utf8mb4
                    collate: utf8mb4_unicode_ci
            default:
                url: '%env(resolve:DATABASE_URL_WRITE)%'
                driver: 'pdo_mysql'
                server_version: '5.7'
                charset: utf8mb4
                default_table_options:
                    charset: utf8mb4
                    collate: utf8mb4_unicode_ci
    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        default_entity_manager: default
        entity_managers:
            filters:
                filters:
                    softdeleteable:
                        class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
                        enabled: true
            read_only:
                connection: read_only
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                mappings:
                    gedmo_loggable:
                        type: annotation
                        prefix: Gedmo\Loggable\Entity
                        dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
                        alias: GedmoLoggable
                        is_bundle: false
                    App:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/vendor/org/comp-bundle/Entity'
                        prefix: 'Org\CompBundle\Entity'
                        alias: Drm
                dql:
                    datetime_functions:
                        timetosec: DoctrineExtensions\Query\Mysql\TimeToSec
                        timediff: DoctrineExtensions\Query\Mysql\TimeDiff
                        now: DoctrineExtensions\Query\Mysql\Now
                    numeric_functions:
                        rand: DoctrineExtensions\Query\Mysql\Rand
            default:
                connection: default
                filters:
                    softdeleteable:
                        class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
                        enabled: true
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                auto_mapping: true
                mappings:
                    gedmo_loggable:
                        type: annotation
                        prefix: Gedmo\Loggable\Entity
                        dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
                        alias: GedmoLoggable
                    App:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/vendor/org/comp-bundle/Entity'
                        prefix: 'Org\CompBundle\Entity'
                        alias: Drm
                dql:
                    datetime_functions:
                        timetosec: DoctrineExtensions\Query\Mysql\TimeToSec
                        timediff: DoctrineExtensions\Query\Mysql\TimeDiff
                        now: DoctrineExtensions\Query\Mysql\Now
                    numeric_functions:
                        rand: DoctrineExtensions\Query\Mysql\Rand

最后但并非最不重要的一点是,我确实有以下设置org\compbundle\Resources\services.yaml

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false
        bind:
            $logger: '@Psr\Log\LoggerInterface'

    Org\CompBundle\Repository\:
        resource: '../../Repository/{Case,Main}'
        exclude: ['../../Repository/**/*Interface.php']
        tags: ['doctrine.repository_service']

    Org\CompBundle\Interfaces\Queues\QueueRepositoryInterface:
        class: Org\CompBundle\Repository\DrmCase\QueueRepository

    Org\CompBundle\Interfaces\Cases\CasesRepositoryInterface:
        class: Org\CompBundle\Repository\DrmCase\CasesRepository

    Doctrine\Common\Persistence\ManagerRegistry: '@doctrine'

到目前为止我做了什么?

  1. 在这里阅读很多关于 SO 的帖子,导致相同的解决方案添加cascade={"persist"}到我所做的拥有方,我必须说没有工作。
  2. 阅读 Doctrine 文档,寻找关于我的实体定义的错误,但到目前为止,我觉得一切都很好。

我确实发现很有帮助,但我一直在问自己,为什么 Doctrine 会仅仅因为引用而尝试插入和存在实体?有什么办法可以让这个工作吗?

我不希望 EntityB 持久化或更新,它已经存在。我确实希望新的 EntityA 对 EntityB 中的现有记录有一个 FK。

标签: symfonydoctrine-ormdoctrinesymfony4

解决方案


在周末,一位工作同事确实非常努力地找出导致问题的原因,经过大量时间调试 Doctrine 和 UoW 的工作原理后,他发现了问题所在:Doctrine 和存储库模式的工作原理。让我稍微解释一下。

如果您仔细检查并深入了解 Doctrine,您会注意到以下代码如何导致两个不同的实体管理器:

public function __construct(RepositoryB $repositoryB, EntityManagerInterface $entityManager)
{
    $this->repositoryB = $repositoryB; // spin his own EM
    $this->entityManager = $entityManager; // this is a new EM object
}

所以:

$entBRef = $this->repositoryB->find(1);

正在通过另一个 EM 获取,并且由于我$this->entityManager用于持久化/刷新最近创建的entityA对象,因此 Doctrine 将$entBRef其视为必须持久化的新实体。

我们的解决方案是通过我不太喜欢的同一个存储库类获取所有内容,因为我们正在查询不属于那里的东西,并且我们不得不使用相同的 EM 持久化/刷新。

如果您有任何其他解决方案,非常欢迎。


推荐阅读