覆盖默认标识符生成策略对关联没有影响
Overriding default identifier generation strategy has no effect on associations
Symfony 2.7.2。学说 ORM 2.4.7。 MySQL 5.6.12。 PHP5.5.0.
我有一个具有自定义 ID 生成器策略的实体。它运行完美。
在某些情况下,我必须使用 "handmade" Id 覆盖此策略。它在没有关联的情况下刷新主要实体时起作用。但它不适用于协会。抛出此示例错误:
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)
重现方法如下:
- Install and create a Symfony2 application.
- 使用您的数据库参数编辑
app/config/parameters.yml
。
使用示例 AppBundle
命名空间,在 src/AppBundle/Entity
目录中创建 Article
和 Tag
实体。
<?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;
}
为上述实体生成 getter 和 setter:
php app/console doctrine:generate:entities AppBundle
在 src/AppBundle/Doctrine
中创建 ArticleNumberGenerator
class:
<?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
.
编辑位于 src\AppBundle\Controller
的示例 AppBundle DefaultController
。将内容替换为:
<?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 次以查看此消息:
Created article id a003r0.
现在,导航至 http://127.0.0.1:8000/create-handmade/test。预期结果是:
Created article id test1.
但您会收到错误消息:
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)
显然是因为 id
"a004r0" 的文章不存在。
如果我在 createHandmadeAction
中注释掉 $article->getTags()->add($tag);
,它会起作用 - 结果是:
Created article id test.
并相应地更新数据库:
id | title
-------+----------------
a001r0 | Test article 204
a002r0 | Test article 12
a003r0 | Test article 549
test | Test article 723
但不是在添加关系时。出于某种原因,Doctrine 不使用手工 id
进行关联,而是使用默认的 Id 生成器策略。
这是怎么回事?如何说服实体管理员使用我手工制作的 ID 进行关联?
您的问题与在更改 ClassMetadata
之前调用 $em->persist($article);
有关。
在持久化新实体时 UnitOfWork
使用 ArticleNumberGenerator
生成 id
并将其保存到 entityIdentifiers
字段中。后来 ManyToManyPersister
在 PersistentCollection
的帮助下使用此值填充关系 table 行。
调用 flush
UoW
计算实体的变更集并保存实际的 id 值 - 这就是为什么在添加关联后得到正确数据的原因。但它不会更新 entityIdentifiers
.
的数据
要解决此问题,您只需将 persist
移到 ClassMetadata 对象的更改后面。但这种方式仍然看起来像 hack。 IMO 更理想的方法是编写自定义生成器,如果提供了分配的 id 或生成新的将使用分配的 id。
PS。另一件应该考虑的事情 - 你的生成 ID 的方式不安全,它会在高负载时产生重复的 ID。
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() . '.');
}
这符合预期。
Symfony 2.7.2。学说 ORM 2.4.7。 MySQL 5.6.12。 PHP5.5.0.
我有一个具有自定义 ID 生成器策略的实体。它运行完美。
在某些情况下,我必须使用 "handmade" Id 覆盖此策略。它在没有关联的情况下刷新主要实体时起作用。但它不适用于协会。抛出此示例错误:
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
, CONSTRAINTFK_354053617294869C
FOREIGN KEY (article_id
) REFERENCESarticle
(id
) ON DELETE CASCADE)
重现方法如下:
- Install and create a Symfony2 application.
- 使用您的数据库参数编辑
app/config/parameters.yml
。 使用示例
AppBundle
命名空间,在src/AppBundle/Entity
目录中创建Article
和Tag
实体。<?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; }
为上述实体生成 getter 和 setter:
php app/console doctrine:generate:entities AppBundle
在
src/AppBundle/Doctrine
中创建ArticleNumberGenerator
class:<?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
. 编辑位于
src\AppBundle\Controller
的示例 AppBundleDefaultController
。将内容替换为:<?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 次以查看此消息:
Created article id a003r0.
现在,导航至 http://127.0.0.1:8000/create-handmade/test。预期结果是:
Created article id test1.
但您会收到错误消息:
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
, CONSTRAINTFK_354053617294869C
FOREIGN KEY (article_id
) REFERENCESarticle
(id
) ON DELETE CASCADE)显然是因为
id
"a004r0" 的文章不存在。
如果我在 createHandmadeAction
中注释掉 $article->getTags()->add($tag);
,它会起作用 - 结果是:
Created article id test.
并相应地更新数据库:
id | title
-------+----------------
a001r0 | Test article 204
a002r0 | Test article 12
a003r0 | Test article 549
test | Test article 723
但不是在添加关系时。出于某种原因,Doctrine 不使用手工 id
进行关联,而是使用默认的 Id 生成器策略。
这是怎么回事?如何说服实体管理员使用我手工制作的 ID 进行关联?
您的问题与在更改 ClassMetadata
之前调用 $em->persist($article);
有关。
在持久化新实体时 UnitOfWork
使用 ArticleNumberGenerator
生成 id
并将其保存到 entityIdentifiers
字段中。后来 ManyToManyPersister
在 PersistentCollection
的帮助下使用此值填充关系 table 行。
调用 flush
UoW
计算实体的变更集并保存实际的 id 值 - 这就是为什么在添加关联后得到正确数据的原因。但它不会更新 entityIdentifiers
.
要解决此问题,您只需将 persist
移到 ClassMetadata 对象的更改后面。但这种方式仍然看起来像 hack。 IMO 更理想的方法是编写自定义生成器,如果提供了分配的 id 或生成新的将使用分配的 id。
PS。另一件应该考虑的事情 - 你的生成 ID 的方式不安全,它会在高负载时产生重复的 ID。
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() . '.');
}
这符合预期。