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:
app/config/parameters.yml
with your DB parameters.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;
}
Generate getters and setters for the above entities:
php app/console doctrine:generate:entities AppBundle
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';
}
}
Create database: php app/console doctrine:database:create
.
php app/console doctrine:schema:create
.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() . '.');
}
}
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?
In this example, we override the generate () method from the IdentifierGenerator interface. First, we want to find the highest number from the existing primary keys of the form prefix-XX. Then we add 1 to the maximum number found and append the prefix property to get the newly generated id value.
Their is Only one rule while doing Method overriding with Access modifiers i.e. If you are overriding any method, overridden method (i.e. declared in subclass) must not be more restrictive. Access modifier restrictions in decreasing order:
Identifiers represent the primary key of an entity and they uniquely identify each entity. I am hoping, you have a good understanding of Entity Mapping. UNIQUE – The value in the DB table is unique.
This implies the values are unique so that they can identify a specific entity, that they aren't null and that they won't be modified. Hibernate provides a few different ways to define identifiers. In this article, we'll review each method of mapping entity ids using the library. 2. Simple Identifiers
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With