Overriding the default identifier generation strategy does not affect associations

Symfony 2.7.2. The doctrine of ORM 2.4.7. MySQL 5.6.12. PHP 5.5.0.
I have an entity with a custom identifier generation system. It works flawlessly.
In some cases, I have to redefine this strategy using "manual work". It works when the main subject is blurred without association. But this does not work with associations. This example error is selected:

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

SQLSTATE [23000]: Integrity constraint violation: 1452 Unable to add or update child row: foreign key constraint failed ( sf-test1 . Articles_tags, CONSTRAINT FK_354053617294869C FOREIGN KEY ( article_id ) LINKS article ( id ) ON DELETE CASCADE)

Here's how to reproduce:

  • Install and create a Symfony2 application .
  • Change app/config/parameters.yml your database parameters.
  • Using the AppBundle namespace, create article and Tag objects in the 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; } 
  • Generate recipients and setters for the following objects:

     php app/console doctrine:generate:entities AppBundle 
  • Create an 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'; } } 
  • Create database: php app/console doctrine:database:create .

  • Create tables: php app/console doctrine:schema:create .
  • Edit the AppBundle DefaultController example located in src\AppBundle\Controller . Replace the contents as follows:

     <?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() . '.'); } } 
  • Start server: php app/console server:run .

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

    Created article id a003r0.

  • Now go to http://127.0.0.1:8000/create-handmade/test . Expected Result:

    Created article id test1.

    but instead you get an error:

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

    SQLSTATE [23000]: Integrity constraint violation: 1452 Unable to add or update child row: foreign key constraint failed ( sf-test1 . Articles_tags, CONSTRAINT FK_354053617294869C FOREIGN KEY ( article_id ) LINKS article ( id ) ON DELETE CASCADE)

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

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

An article ID test has been created.

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 adding a relationship. For some reason, Doctrine does not use handmade id for associations; instead, it uses the default Id generator strategy.

What is wrong here? How to convince an entity manager to use my hand identifiers for associations?

+8
oop php symfony associations doctrine2
Jul 23 '15 at 17:32
source share
1 answer

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

When a new UnitOfWork object is UnitOfWork , an id is created using ArticleNumberGenerator and stored in the entityIdentifiers field. ManyToManyPersister uses this value with the PersistentCollection when populating the row of the relationship table.

When flush called, UoW computes the set of changes to the object and stores the actual value of the identifier - therefore, you get the correct data after you join the association with the addition of the association. But it does not update entityIdentifiers data.

To fix this, you can simply move persist to modify the ClassMetadata object. But the path still looks like a hack. IMO, a more optimal way is to create a custom generator that will use the assigned identifier, if provided or to generate a new one.

PS . Another thing to keep in mind is that your generation identifier path is unsafe, it will produce duplicate identifiers at high load.

UPD I UoW that UoW does not use idGeneratorType (it is used by factory metadata to set the correct idGenerator value), so you must set the correct 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 as expected.

+4
Aug 05 '15 at 14:34
source share



All Articles