重写默认的标识符生成策略对联想无影响 [英] Overriding default identifier generation strategy has no effect on associations

查看:246
本文介绍了重写默认的标识符生成策略对联想无影响的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Symfony的2.7.2。学说ORM 2.4.7。 MySQL的5.6.12。 PHP 5.5.0。结果
我有一个自定义ID生成策略的实体。它完美的作品。结果
在某些情况下,我不得不覆盖这个策略以手工制作标识。它的工作原理当主实体被无关联刷新。不过,这并不与各协会工作。这个例子引发错误:


  

在执行'(?,?)INSERT INTO articles_tags(article_id的,TAG_ID)VALUES使用参数[a004r0,4]出现异常
  
  

SQLSTATE [23000]:完整性约束冲突:1452不能添加或更新子行:外键约束失败( SF-测试1 articles_tags ,约束 FK_354053617294869C 外键(的article_id )参考文章 ID )ON DELETE CASCADE)


下面是如何重现:


  1. Install并创建一个应用程序的Symfony2

  2. 编辑应用程序/配置/ parameters.yml 与你的数据库的参数。

  3. 使用示例的appbundle 命名空间,创建文章标记在实体的src /的appbundle /实体目录。

     < PHP
    // SRC /的appbundle /实体/ Article.php
    命名空间的appbundle \\实体;使用Doctrine \\ ORM \\映射作为ORM;/ **
     * @ORM \\实体
     * @ORM \\表(名称=文章)
     * /
    类文章
    {
        / **
         * @ORM \\列(类型=字符串)
         * @ORM \\标识
         * @ORM \\ GeneratedValue(策略=CUSTOM)
         * @ORM \\ CustomIdGenerator(类=的appbundle \\主义\\ ArticleNumberGenerator)
         * /
        保护的$ id;    / **
         * @ORM \\列(类型=字符串,长度= 255)
         * /
        保护$称号;    / **
         * @ORM \\多对多(targetEntity =标签,inversedBy =物品,级联= {所有})
         * @ORM \\ JoinTable(NAME =articles_tags)
         ** /
        私人$标签;    公共职能SETID($ ID)
        {
            $这个 - > ID = $ ID;
        }
    }

     < PHP
    // SRC /的appbundle /实体/ Tag.php
    命名空间的appbundle \\实体;使用Doctrine \\ ORM \\映射作为ORM;
    使用Doctrine \\ COMMON \\收藏\\ ArrayCollection的;/ **
     * @ORM \\实体
     * @ORM \\表(名称=标签)
     * /
    类标签
    {
        / **
         * @ORM \\列(类型=整数)
         * @ORM \\标识
         * @ORM \\ GeneratedValue
         * /
        保护的$ id;    / **
         * @ORM \\列(类型=字符串,长度= 255)
         * /
        保护$名称​​;    / **
         * @ORM \\多对多(targetEntity =文章的mappedBy =标签)
         ** /
        私人物品$;
    }


  4. 生成对上述实体getter和setter方法​​:

      PHP应用程序/控制台学说:生成:实体的appbundle


  5. 创建 ArticleNumberGenerator 的src /的appbundle /学说

     < PHP
    // SRC /的appbundle /教义/ ArticleNumberGenerator.php
    命名空间的appbundle \\主义;
    使用Doctrine \\ ORM \\标识\\ AbstractIdGenerator;
    使用Doctrine \\ ORM \\查询\\ ResultSetMapping;类ArticleNumberGenerator扩展AbstractIdGenerator
    {
        公共函数生成(\\学说\\ ORM \\ $的EntityManager他们,$实体)
        {
            $ RSM =新ResultSetMapping();
            $ rsm-> addScalarResult('身份证','文章','串');
            $查询= $的EM> createNativeQuery('选择MAX(`id`)从`article` id其中`id`喜欢:id_pattern',$ RSM);
            $查询 - >的setParameter('id_pattern','a___r_');
            $ IDMAX =(int)的SUBSTR($查询 - > getSingleScalarResult(),1,3);
            $ IDMAX ++;
            回归'A'。 str_pad($ IDMAX,3,'0',STR_PAD_LEFT)。 R0;
        }
    }


  6. 创建数据库: PHP应用程序/控制台学说:数据库:创建


  7. 创建表: PHP应用程序/控制台学说:模式:创建

  8. 编辑例子的appbundle DefaultController 位于的src \\的appbundle \\控制器。替换为内容:

     < PHP
    // SRC /的appbundle /控制器/ DefaultController.php
    命名空间的appbundle \\控制器;使用SENSIO \\包\\ FrameworkExtraBundle \\配置\\路线;
    使用的Symfony \\包\\ FrameworkBundle \\控制器\\控制器;
    使用的Symfony \\分量\\ HttpFoundation \\请求;
    使用的Symfony \\分量\\ HttpFoundation \\响应;使用的appbundle \\实体\\文章;
    使用的appbundle \\实体\\标签;类DefaultController扩展控制器
    {
        / **
         * @Route(/创建默认)
         * /
        公共职能createDefaultAction()
        {
            $标签=新标签();
            $标签也即>的setName('标签'兰特(1,99));        $文章=新的第();
            $物品─>的setTitle(测试文章'兰特(1,999));
            $物品─> getTags() - >添加($标签);        $ EM = $这个 - > getDoctrine() - GT; getManager();        $的EM>的getConnection() - GT;的BeginTransaction();
            $的EM>坚持($条);        尝试{
                $的EM>的flush();
                $的EM>的getConnection() - GT;提交();
            }赶上(\\ $的RuntimeException E){
                $的EM>的getConnection() - GT;回滚();
                扔$ê;
            }        返回新的响应('创建文章编号$物品─方式>的getId()。'');
        }    / **
         * @Route(/创建-手工/ {}手工)
         * /
        公共职能createHandmadeAction($手工制作)
        {
            $标签=新标签();
            $标签也即>的setName('标签'兰特(1,99));        $文章=新的第();
            $物品─>的setTitle(测试文章'兰特(1,999));
            $物品─> getTags() - >添加($标签);        $ EM = $这个 - > getDoctrine() - GT; getManager();        $的EM>的getConnection() - GT;的BeginTransaction();
            $的EM>坚持($条);        $元= $的EM> getClassMetadata(get_class($条));
            $元数据 - > setIdGeneratorType(\\学说\\ ORM \\制图\\ ClassMetadata :: GENERATOR_TYPE_NONE);
            $物品─> SETID($手工制作);        尝试{
                $的EM>的flush();
                $的EM>的getConnection() - GT;提交();
            }赶上(\\ $的RuntimeException E){
                $的EM>的getConnection() - GT;回滚();
                扔$ê;
            }        返回新的响应('创建文章编号$物品─方式>的getId()。'');
        }
    }


  9. 运行服务器: PHP应用程序/控制台服务器:运行


  10. 导航到 http://127.0.0.1:8000/create-default 。刷新2次看到这样的消息:


      

    创建文章编号a003r0。



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


      

    创建文章编号test1的。


    而是你会得到错误:


      

    在执行'(?,?)INSERT INTO articles_tags(article_id的,TAG_ID)VALUES使用参数[a004r0,4]出现异常
      
      

    SQLSTATE [23000]:完整性约束冲突:1452不能添加或更新子行:外键约束失败( SF-测试1 articles_tags ,约束 FK_354053617294869C 外键(的article_id )参考文章 ID )ON DELETE CASCADE)


    显然是因为与文章 ID a004r0不存在。


如果我注释掉 $物品─> getTags() - >添加($标签); createHandmadeAction ,它的工作原理 - 结果是:


  

创建文章编号测试。


和数据库进行相应的更新:

ID |标题
------- + ----------------
a001r0 |测试204条
a002r0 |测试第12条
a003r0 |测试549条
测试|测试723条

但不添加时的关系。对于原因,学说不使用手工制作的 ID 的关联,而是使用默认的ID生成策略。

这里有什么不对吗?如何说服实体管理器使用我的手工IDS的关联?


坚持($条); 改变之前
解决方案

您的问题与 $的EM&GT的呼叫相关在 ClassMetadata

在新的实体持续的UnitOfWork 生成 ID ArticleNumberGenerator ,并将其保存到 entityIdentifiers 字段。后来 ManyToManyPersister 使用带有 PersistentCollection 的帮助下对关系表行填充此值。

在调用刷新 UOW 计算变更集实体并保存值的实际ID - 这就是为什么你commeting了加入协会后,得到正确的数据。不过,这并不更新 entityIdentifiers 数据。

要解决这个问题,你可以只移动坚持落后了ClassMetadata对象的变化。但这样仍然看起来像黑客。 IMO更优的方法是编写将使用分配的ID如果提供一个或产生新的自定义生成器。

PS 。应考虑的另一件事情 - 你对代身份证的方式并不安全,这将在高负荷产生重复的ID。

UPD
错过了 UOW 不使用 idGeneratorType (它由元数据出厂设置适当的 idGenerator 值),所以你应该设置适当的 idGenerator

  / **
 * @Route(/创建-手工/ {}手工)
 * /
公共职能createHandmadeAction($手工制作)
{
    $标签=新标签();
    $标签也即>的setName('标签'兰特(1,99));    $文章=新的第();
    $物品─>的setTitle(测试文章'兰特(1,999));
    $物品─> getTags() - >添加($标签);    $ EM = $这个 - > getDoctrine() - GT; getManager();    $的EM>的getConnection() - GT;的BeginTransaction();    $元= $的EM> getClassMetadata(get_class($条));
    $元数据 - > setIdGeneratorType(\\学说\\ ORM \\制图\\ ClassMetadata :: GENERATOR_TYPE_NONE);
    $元数据 - > setIdGenerator(新\\学说\\ ORM \\标识\\ AssignedGenerator());
    $物品─> SETID($手工制作);    $的EM>坚持($条);    尝试{
        $的EM>的flush();
        $的EM>的getConnection() - GT;提交();
    }赶上(\\ $的RuntimeException E){
        $的EM>的getConnection() - GT;回滚();
        扔$ê;
    }    返回新的响应('创建文章编号$物品─方式>的getId()。'');
}

这就像预期的。

Symfony 2.7.2. Doctrine ORM 2.4.7. MySQL 5.6.12. PHP 5.5.0.
I have an entity with custom ID generator strategy. It works flawlessly.
In some circumstances I have to override this strategy with a "handmade" Id. It works when the main entity is being flushed without associations. But it doesn't work with associations. This example error is thrown:

An exception occurred while executing 'INSERT INTO articles_tags (article_id, tag_id) VALUES (?, ?)' with params ["a004r0", 4]:

SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (sf-test1.articles_tags, CONSTRAINT FK_354053617294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE)

Here's how to reproduce:

  1. Install and create a Symfony2 application.
  2. Edit app/config/parameters.yml with your DB parameters.
  3. Using the example AppBundle namespace, create Article and Tag entities in src/AppBundle/Entity directory.

    <?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;
        }
    }
    

    <?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;
    }
    

  4. Generate getters and setters for the above entities:

    php app/console doctrine:generate:entities AppBundle
    

  5. Create ArticleNumberGenerator class in src/AppBundle/Doctrine:

    <?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';
        }
    }
    

  6. Create database: php app/console doctrine:database:create.

  7. Create tables: php app/console doctrine:schema:create.
  8. Edit the example AppBundle DefaultController located in src\AppBundle\Controller. Replace the content with:

    <?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() . '.');
        }
    }
    

  9. Run server: php app/console server:run.

  10. Navigate to http://127.0.0.1:8000/create-default. Refresh 2 times to see this message:

    Created article id a003r0.

  11. Now, navigate to http://127.0.0.1:8000/create-handmade/test. The expected result is:

    Created article id test1.

    but instead you'll get the error:

    An exception occurred while executing 'INSERT INTO articles_tags (article_id, tag_id) VALUES (?, ?)' with params ["a004r0", 4]:

    SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (sf-test1.articles_tags, CONSTRAINT FK_354053617294869C FOREIGN KEY (article_id) REFERENCES article (id) ON DELETE CASCADE)

    obviously because article with id "a004r0" does not exist.

If I comment-out $article->getTags()->add($tag); in createHandmadeAction, it works - the result is:

Created article id test.

and the database is updated accordingly:

id     | title
-------+----------------
a001r0 | Test article 204
a002r0 | Test article 12
a003r0 | Test article 549
test   | Test article 723

but not when a relationship is added. For a reason, Doctrine does not use the handmade id for associations, instead it uses the default Id generator strategy.

What's wrong here? How to convince the entity manager to use my handmade Ids for associations?

解决方案

Your problem is related with calling of $em->persist($article); before changing the ClassMetadata.

On persisting of new entity UnitOfWork generates id with ArticleNumberGenerator and saves it into entityIdentifiers field. Later ManyToManyPersister uses this value with help of PersistentCollection on filling of relation table row.

On calling flush UoW computes the change set of the entity and saves the actual id value - that's why you get correct data after commeting out of adding association. But it doesn't update data of entityIdentifiers.

To fix this you could just move persist behind changing of the ClassMetadata object. But the way still looks like hack. IMO the more optimal way is to write the custom generator that will use assigned id if the one is provided or to generate new.

PS. The another thing that should be taken into account - your way of the generation id is not safe, it will produce duplicated ids upon the high-load.

UPD Missed that UoW doesn't use idGeneratorType (it is used by metadata factory to set proper idGenerator value) so you should set proper 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() . '.');
}

This works like expected.

这篇关于重写默认的标识符生成策略对联想无影响的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆