2015-07-23 13 views
8

Symfony 2.7.2. Doctrine ORM 2.4.7. MySQL 5.6.12. PHP 5.5.0.
Ho un'entità con strategia generatore ID personalizzato. Funziona perfettamente.
In alcune circostanze devo scavalcare questa strategia con un ID "fatto a mano". Funziona quando l'entità principale viene svuotata senza associazioni. Ma non funziona con le associazioni. Questo errore è gettato esempio:L'override della strategia di generazione dell'identificatore predefinito non ha alcun effetto sulle associazioni

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)

Ecco come riprodurre:

  1. Install and create a Symfony2 application.
  2. Modifica app/config/parameters.yml con i parametri del DB.
  3. Utilizzando lo spazio dei nomi AppBundle, creare Article e Tag nella directory src/AppBundle/Entity.

    <?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. Generare getter e setter per gli enti di cui sopra:

    php app/console doctrine:generate:entities AppBundle 
    
  5. Creare ArticleNumberGenerator classe 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. Creare da tabase: .

  7. Creare tabelle: php app/console doctrine:schema:create.
  8. Modificare l'esempio AppBundle DefaultController situato in src\AppBundle\Controller. Sostituire il contenuto con: server di

    <?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: php app/console server:run.

  10. Passare a http://127.0.0.1:8000/create-default. Refresh 2 volte per vedere questo messaggio:

    Created article id a003r0.

  11. Ora, passare a http://127.0.0.1:8000/create-handmade/test. Il risultato atteso è:

    Created article id test1.

    ma invece si otterrà l'errore:

    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)

    , ovviamente, perché l'articolo con id "a004r0" non esiste.

Se io commento-out $article->getTags()->add($tag); in createHandmadeAction, funziona - il risultato è:

Created article id test.

e il database viene aggiornato di conseguenza:

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

ma non quando un rapporto è aggiunto. Per una ragione, Doctrine non usa il id fatto a mano per le associazioni, ma utilizza la strategia di generatore Id predefinita.

Cosa c'è che non va qui? Come convincere il gestore di entità a utilizzare i miei ID fatti a mano per le associazioni?

risposta

4

prima di modificare lo ClassMetadata, il problema è legato al richiamo di $em->persist($article);.

Su persistere del nuovo soggetto UnitOfWork genera id con ArticleNumberGenerator e lo salva in entityIdentifiers campo. Successivamente ManyToManyPersister utilizza questo valore con l'aiuto di PersistentCollection per il riempimento della riga della tabella delle relazioni.

Alla chiamata flushUoW calcola il set di modifiche dell'entità e salva il valore effettivo dell'ID: ecco perché si ottengono dati corretti dopo l'aggiunta di associazione. Ma non aggiorna i dati di entityIdentifiers.

Per risolvere questo problema, è sufficiente spostare persist dietro la modifica dell'oggetto ClassMetadata. Ma il modo in cui appare ancora hack. IMO il modo più ottimale è scrivere il generatore personalizzato che utilizzerà l'ID assegnato se viene fornito uno o per generare nuovo.

PS. L'altra cosa che dovrebbe essere presa in considerazione - il tuo modo di id della generazione non è sicuro, produrrà id duplicati sul carico elevato.

UPD perso che UoW non usa idGeneratorType (è usato dalla fabbrica di metadati per impostare una corretta valore idGenerator) così si dovrebbe impostare una corretta 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() . '.'); 
} 

Questo funziona come previsto.

+0

Questo è tutto. Testato in un'app di produzione con tutti i tipi di associazioni: funziona. Grazie! – bostaf

Problemi correlati