问题
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
, CONSTRAINTFK_354053617294869C
FOREIGN KEY (article_id
) REFERENCESarticle
(id
) ON DELETE CASCADE)
Here's how to reproduce:
- Install and create a Symfony2 application.
- Edit
app/config/parameters.yml
with your DB parameters. Using the example
AppBundle
namespace, createArticle
andTag
entities insrc/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 getters and setters for the above entities:
php app/console doctrine:generate:entities AppBundle
Create
ArticleNumberGenerator
class insrc/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 example AppBundle
DefaultController
located insrc\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() . '.'); } }
Run server:
php app/console server:run
.Navigate to http://127.0.0.1:8000/create-default. Refresh 2 times to see this message:
Created article id a003r0.
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
, CONSTRAINTFK_354053617294869C
FOREIGN KEY (article_id
) REFERENCESarticle
(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?
回答1:
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.
来源:https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations