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]:
- Symfony / PHP - лучший способ сохранить одно значение
- Symfony2 - Doctrine2 QueryBuilder ГДЕ В поле ManyToMany
- Использование jQuery в шаблоне php Symfony2
- Выйти из системы в приложении Symfony 2, когда включена опция «запомнить меня»
- Как добавить настраиваемое исключение в symfony2
SQLSTATE [23000]: нарушение ограничения целостности: 1452 Невозможно добавить или обновить дочернюю строку: ограничение внешнего ключа завершается с ошибкой (
sf-test1
. Articles_tags, CONSTRAINTFK_354053617294869C
ИНОСТРАННЫЙ КЛЮЧ (article_id
) СПРАВОЧНАЯ ИНФОРМАЦИЯarticle
(id
) ON DELETE CASCADE)
Вот как воспроизвести:
app/config/parameters.yml
с параметрами DB. Используя пример пространства имен 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; }
Генерировать получатели и сеттеры для указанных объектов:
php app/console doctrine:generate:entities AppBundle
Создать класс 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'; } }
Создать базу данных: php app/console doctrine:database:create
.
php app/console doctrine:schema:create
. Отредактируйте пример 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() . '.'); } }
Запустить сервер: php app/console server:run
.
Перейдите к http://127.0.0.1:8000/create-default . Обновите 2 раза, чтобы увидеть это сообщение:
Создан код статьи a003r0.
Теперь перейдите к 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, CONSTRAINTFK_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() . '.'); }
Это работает, как и ожидалось.