Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding default identifier generation strategy has no effect on associations

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?

like image 529
Taz Avatar asked Jul 23 '15 17:07

Taz


People also ask

How do I override the generate () method from the identifiergenerator interface?

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.

What are the rules for method overriding with access modifiers?

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:

What is identifier in Entity Mapping?

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.

Why are hibernate entity IDs 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


1 Answers

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.

like image 77
origaminal Avatar answered Oct 15 '22 05:10

origaminal