覆盖默认标识符生成策略对关联没有影响

时间:2015-07-23 17:32:00

标签: php symfony oop doctrine-orm associations

Symfony 2.7.2。 Doctrine ORM 2.4.7。 MySQL 5.6.12。 PHP 5.5.0。
我有一个具有自定义ID生成器策略的实体。它完美无缺 在某些情况下,我必须用“手工制作”ID覆盖此策略。它在主要实体被刷新而没有关联时起作用。但它不适用于协会。抛出此示例错误:

  

使用params [“a004r0”,4]执行'INSERT INTO articles_tags(article_id,tag_id)VALUES(?,?)'时发生异常:

     

SQLSTATE [23000]:完整性约束违规:1452无法添加或更新子行:外键约束失败(sf-test1articles_tags,CONSTRAINT FK_354053617294869C FOREIGN KEY({{ 1}})参考article_idarticle)ON DELETE CASCADE)

以下是如何重现:

  1. Install and create a Symfony2 application
  2. 使用您的数据库参数修改id
  3. 使用示例app/config/parameters.yml命名空间,在AppBundle目录中创建ArticleTag个实体。

    src/AppBundle/Entity
    <?php
    // src/AppBundle/Entity/Article.php
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity
     * @ORM\Table(name="article")
     */
    class Article
    {
        /**
         * @ORM\Column(type="string")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="CUSTOM")
         * @ORM\CustomIdGenerator(class="AppBundle\Doctrine\ArticleNumberGenerator")
         */
        protected $id;
    
        /**
         * @ORM\Column(type="string", length=255)
         */
        protected $title;
    
        /**
         * @ORM\ManyToMany(targetEntity="Tag", inversedBy="articles" ,cascade={"all"})
         * @ORM\JoinTable(name="articles_tags")
         **/
        private $tags;
    
        public function setId($id)
        {
            $this->id = $id;
        }
    }
    
  4. 为上述实体生成getter和setter:

    <?php
    // src/AppBundle/Entity/Tag.php
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use Doctrine\Common\Collections\ArrayCollection;
    
    /**
     * @ORM\Entity
     * @ORM\Table(name="tag")
     */
    class Tag
    {
        /**
         * @ORM\Column(type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue
         */
        protected $id;
    
        /**
         * @ORM\Column(type="string", length=255)
         */
        protected $name;
    
        /**
         * @ORM\ManyToMany(targetEntity="Article", mappedBy="tags")
         **/
        private $articles;
    }
    
  5. php app/console doctrine:generate:entities AppBundle 中创建ArticleNumberGenerator课程:

    src/AppBundle/Doctrine
  6. 创建数据库:<?php // src/AppBundle/Doctrine/ArticleNumberGenerator.php namespace AppBundle\Doctrine; use Doctrine\ORM\Id\AbstractIdGenerator; use Doctrine\ORM\Query\ResultSetMapping; class ArticleNumberGenerator extends AbstractIdGenerator { public function generate(\Doctrine\ORM\EntityManager $em, $entity) { $rsm = new ResultSetMapping(); $rsm->addScalarResult('id', 'article', 'string'); $query = $em->createNativeQuery('select max(`id`) as id from `article` where `id` like :id_pattern', $rsm); $query->setParameter('id_pattern', 'a___r_'); $idMax = (int) substr($query->getSingleScalarResult(), 1, 3); $idMax++; return 'a' . str_pad($idMax, 3, '0', STR_PAD_LEFT) . 'r0'; } }

  7. 创建表格:php app/console doctrine:database:create
  8. 修改位于php app/console doctrine:schema:create的示例AppBundle DefaultController。将内容替换为:

    src\AppBundle\Controller
  9. 运行服务器:<?php // src/AppBundle/Controller/DefaultController.php namespace AppBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use AppBundle\Entity\Article; use AppBundle\Entity\Tag; class DefaultController extends Controller { /** * @Route("/create-default") */ public function createDefaultAction() { $tag = new Tag(); $tag->setName('Tag ' . rand(1, 99)); $article = new Article(); $article->setTitle('Test article ' . rand(1, 999)); $article->getTags()->add($tag); $em = $this->getDoctrine()->getManager(); $em->getConnection()->beginTransaction(); $em->persist($article); try { $em->flush(); $em->getConnection()->commit(); } catch (\RuntimeException $e) { $em->getConnection()->rollBack(); throw $e; } return new Response('Created article id ' . $article->getId() . '.'); } /** * @Route("/create-handmade/{handmade}") */ public function createHandmadeAction($handmade) { $tag = new Tag(); $tag->setName('Tag ' . rand(1, 99)); $article = new Article(); $article->setTitle('Test article ' . rand(1, 999)); $article->getTags()->add($tag); $em = $this->getDoctrine()->getManager(); $em->getConnection()->beginTransaction(); $em->persist($article); $metadata = $em->getClassMetadata(get_class($article)); $metadata->setIdGeneratorType(\Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_NONE); $article->setId($handmade); try { $em->flush(); $em->getConnection()->commit(); } catch (\RuntimeException $e) { $em->getConnection()->rollBack(); throw $e; } return new Response('Created article id ' . $article->getId() . '.'); } }

  10. 导航至http://127.0.0.1:8000/create-default。刷新2次以查看此消息:

      

    创建文章ID a003r0。

  11. 现在,导航至http://127.0.0.1:8000/create-handmade/test。预期结果是:

      

    创建文章ID test1。

    但你会得到错误:

      

    使用params [“a004r0”,4]执行'INSERT INTO articles_tags(article_id,tag_id)VALUES(?,?)'时发生异常:

         

    SQLSTATE [23000]:完整性约束违规:1452无法添加或更新子行:外键约束失败(php app/console server:runsf-test1,CONSTRAINT articles_tags FOREIGN KEY({{ 1}})参考FK_354053617294869Carticle_id)ON DELETE CASCADE)

    显然因为article“a004r0”的文章不存在。

  12. 如果我在id中对id发表评论,那么就可以了 - 结果是:

      

    创建文章内容测试。

    并相应地更新数据库:

    $article->getTags()->add($tag);

    但不是在添加关系时。出于某种原因,Doctrine不使用手工制作的createHandmadeAction作为关联,而是使用默认的Id生成器策略。

    这里有什么问题?如何说服实体经理将我的手工制作的ID用于协会?

1 个答案:

答案 0 :(得分:4)

在更改$em->persist($article);之前,您的问题与调用ClassMetadata有关。

持久保存新实体UnitOfWork会生成id ArticleNumberGenerator并将其保存到entityIdentifiers字段中。之后ManyToManyPersisterPersistentCollection的帮助下使用此值来填充关系表行。

在调用flush UoW时,会计算实体的更改集并保存实际的ID值 - 这就是为什么在纪念添加关联后获得正确数据的原因。但它不会更新entityIdentifiers的数据。

要解决此问题,您只需移动persist,即可更改ClassMetadata对象。但方式仍然看起来像黑客。 IMO更优化的方法是编写自定义生成器,如果提供了自定义生成器,则使用已分配的id或生成新的。

<强> PS 即可。另一件应该考虑的事情 - 你的生成方式不安全,它会在高负载时产生重复的ID。

<强> UPD 错过了UoW没有使用idGeneratorType(元数据工厂使用它来设置正确的idGenerator值)因此您应该设置正确的idGenerator

/**
 * @Route("/create-handmade/{handmade}")
 */
public function createHandmadeAction($handmade)
{
    $tag = new Tag();
    $tag->setName('Tag ' . rand(1, 99));

    $article = new Article();
    $article->setTitle('Test article ' . rand(1, 999));
    $article->getTags()->add($tag);

    $em = $this->getDoctrine()->getManager();

    $em->getConnection()->beginTransaction();

    $metadata = $em->getClassMetadata(get_class($article));
    $metadata->setIdGeneratorType(\Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_NONE);
    $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator());
    $article->setId($handmade);

    $em->persist($article);

    try {
        $em->flush();
        $em->getConnection()->commit();
    } catch (\RuntimeException $e) {
        $em->getConnection()->rollBack();
        throw $e;
    }

    return new Response('Created article id ' . $article->getId() . '.');
}

这就像预期的那样。