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
) НА УДАЛЕННОМ КАСКАДЕ)
Вот как воспроизвести:
app/config/parameters.yml
с вашими параметрами БД.Используя пример 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
, ОГРАНИЧЕНИЕ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 по умолчанию.
Что здесь не так? Как убедить менеджера организации использовать мои идентификаторы ручной работы для ассоциаций?
Ваша проблема связана с вызовом $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() . '.');
}
Это работает как ожидалось.
Других решений пока нет …