Переопределение стратегии генерации идентификатора по умолчанию не влияет на ассоциации

Symfony 2.7.2. Учение ОРМ 2.4.7. MySQL 5.6.12. PHP 5.5.0.
У меня есть сущность с пользовательской стратегией генератора идентификаторов. Работает без нареканий.
В некоторых случаях я должен переопределить эту стратегию с помощью идентификатора «ручной работы». Это работает, когда основной объект сбрасывается без ассоциаций. Но это не работает с ассоциациями. В этом примере выдается ошибка:

Возникла исключительная ситуация при выполнении ‘INSERT INTO articles_tags (article_id, tag_id) VALUES (?,?)’ С параметрами [«a004r0», 4]:

SQLSTATE [23000]: Нарушение ограничения целостности: 1452 Невозможно добавить или обновить дочернюю строку: ограничение внешнего ключа не выполнено (sf-test1,articles_tags, ОГРАНИЧЕНИЕ FK_354053617294869C ИНОСТРАННЫЙ КЛЮЧ (article_id) РЕКОМЕНДАЦИИ article (id) НА УДАЛЕННОМ КАСКАДЕ)

Вот как воспроизвести:

  1. Установите и создайте приложение Symfony2.
  2. редактировать app/config/parameters.yml с вашими параметрами БД.
  3. Используя пример AppBundle пространство имен, создать Article а также Tag юридические лица в 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. Сгенерируйте геттеры и сеттеры для вышеуказанных объектов:

    php app/console doctrine:generate:entities AppBundle
    
  5. Создайте ArticleNumberGenerator класс в 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. Создать базу данных: php app/console doctrine:database:create,

  7. Создать таблицы: php app/console doctrine:schema:create,
  8. Изменить пример AppBundle DefaultController находится в src\AppBundle\Controller, Заменить содержимое на:

    <?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. Запустить сервер: php app/console server:run,

  10. Перейдите к http://127.0.0.1:8000/create-default. Обновите 2 раза, чтобы увидеть это сообщение:

    Создан идентификатор статьи a003r0.

  11. Теперь перейдите к http://127.0.0.1:8000/create-handmade/test. Ожидаемый результат:

    Создан идентификатор статьи test1.

    но вместо этого вы получите ошибку:

    Возникла исключительная ситуация при выполнении ‘INSERT INTO articles_tags (article_id, tag_id) VALUES (?,?)’ С параметрами [«a004r0», 4]:

    SQLSTATE [23000]: Нарушение ограничения целостности: 1452 Невозможно добавить или обновить дочернюю строку: ограничение внешнего ключа не выполнено (sf-test1,articles_tags, ОГРАНИЧЕНИЕ FK_354053617294869C ИНОСТРАННЫЙ КЛЮЧ (article_id) РЕКОМЕНДАЦИИ article (id) НА УДАЛЕННОМ КАСКАДЕ)

    очевидно, потому что статья с id «a004r0» не существует.

Если я закомментирую $article->getTags()->add($tag); в createHandmadeAction, это работает — результат:

Создан тест идентификатора статьи.

и база данных обновляется соответственно:

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

но не когда отношения добавляются. По какой-то причине, Doctrine не использует ручной работы id для ассоциаций вместо этого используется стратегия генератора Id по умолчанию.

Что здесь не так? Как убедить менеджера организации использовать мои идентификаторы ручной работы для ассоциаций?

8

Решение

Ваша проблема связана с вызовом $em->persist($article); перед изменением ClassMetadata,

О сохранении новой сущности UnitOfWork генерирует id с ArticleNumberGenerator и сохраняет его в entityIdentifiers поле. Потом ManyToManyPersister использует это значение с помощью PersistentCollection по заполнению строки таблицы отношений.

По вызову flush UoW вычисляет набор изменений сущности и сохраняет фактическое значение идентификатора — поэтому вы получаете правильные данные после извлечения из добавления ассоциации. Но это не обновляет данные entityIdentifiers,

Чтобы исправить это, вы можете просто двигаться persist за изменением объекта ClassMetadata. Но путь все равно выглядит как взломать. IMO, более оптимальный способ — написать собственный генератор, который будет использовать назначенный идентификатор, если он указан, или генерировать новый.

PS. Еще одна вещь, которая должна быть принята во внимание — ваш способ генерации идентификатора небезопасен, он будет создавать дублированные идентификаторы при высокой нагрузке.

UPD
Пропустил что UoW не использует idGeneratorType (используется фабрикой метаданных для установки правильного idGenerator значение), поэтому вы должны установить правильное 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() . '.');
}

Это работает как ожидалось.

4

Другие решения

Других решений пока нет …