Intereting Posts
Загрузка многостраничного изображения на PHP-сервер из приложения iOS Как лучше всего в PHP читать последние строки из файла? Выберите произвольный файл, используя OPENDIR () Mysqli – При открытии двух соединений с одним и тем же хостом с mysql, но с разными базами данных, mysqli открывает соединение один раз? Загрузка нескольких файлов в php Кэш-файл кэширует сетевые файлы Загрузка файла удаленного сервера через PHP Как сортировать массив значения по альфа-ставке в php, используя функцию asort () () Можно ли отключить / скрыть уведомления PHP? Как получить доступ к свойствам объекта с именами, такими как целые числа? Symfony 2 присоединяется к неработающей доктрине Рекурсивная функция PHP не возвращает значение preg_match с международными символами и акцентами Поддерживает ли JSON «поддержку конверсии родных типов? установить переменные в глобальную область внутри цикла

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

Symfony 2.7.2. Доктрина ORM 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, CONSTRAINT FK_354053617294869C ИНОСТРАННЫЙ КЛЮЧ ( article_id ) СПРАВОЧНАЯ ИНФОРМАЦИЯ article ( id ) ON DELETE CASCADE)

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

  1. Установите и создайте приложение Symfony2 .
  2. Измените app/config/parameters.yml с параметрами DB.
  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, CONSTRAINT FK_354053617294869C ИНОСТРАННЫЙ КЛЮЧ ( article_id ) СПРАВОЧНАЯ ИНФОРМАЦИЯ article ( id ) ON DELETE CASCADE)

    очевидно, потому что статьи с 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 по умолчанию.

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

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

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

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

Чтобы исправить это, вы можете просто перемещаться, сохраняя за собой изменение объекта ClassMetadata. Но путь по-прежнему выглядит как хак. ИМО более оптимальным способом является создание пользовательского генератора, который будет использовать назначенный идентификатор, если он предоставлен или для генерации нового.

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() . '.'); } 

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