问题描述
问题
当 Doctrine 在 cascade={"persist"}
关系上使用 ManyToOne
持久化时,有没有一种方法可以识别现有对象,并且在尝试再次插入它时不会失败从而违反唯一键规则?
说明
为此,我的实体上有以下代码:
/**
* Abstract class representing a location
*
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\discriminatorColumn(name="type",type="string")
* @ORM\discriminatorMap({"COUNTRY" = "CountryLocation","REGION" = "RegionLocation","DEPARTMENT" = "DepartmentLocation"})
* @ORM\Table(name="ss_locations")
*
* @package Locations
*/
abstract class ALocation {
/**
* A type that determines the location type
*/
protected ?string $type = null;
/**
* The location ID
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer",options={"unsigned"=true})
*
* @var int
*/
protected int $id;
/**
* The location's slug identifier
*
* @ORM\Column(type="string")
*
* @example "Pays de la Loire" region's slug will be "pays-de-la-loire"
*
* @var string
*/
protected string $slug;
/**
* The location path through its parent's slugs
*
* @ORM\Column(type="string",unique=true)
*
* @example "Loire-Atlantique" department's path would be "france/pays-de-la-loire/loire-atlantique"
*
* @var string
*/
protected string $path;
/**
* The name location's
*
* @ORM\Column(type="string")
*
* @var string
*/
protected string $name;
/**
* The parent location instance
*
* @ORM\ManyToOne(targetEntity="ALocation",cascade={"persist"})
* @ORM\JoinColumn(name="parent",referencedColumnName="id")
*
* @var ALocation|null
*/
protected ?ALocation $parent = null;
// ...
}
// Example of child class
/**
* Class DepartmentLocation
*
* @ORM\Entity
*
* @package Locations
*/
class DepartmentLocation extends ALocation {
const TYPE = "DEPARTMENT";
/**
* @inheritdoc
*/
protected ?string $type = "DEPARTMENT";
// ...
}
表创建进展顺利,但是当我尝试保留一个位置时,我遇到了这些错误:
sqlSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'FR' for key 'PRIMARY')
Warning: Île-de-France cannot be inserted in DB : reason(An exception occurred while executing 'INSERT INTO ss_locations (iso_code,name,parent_id,type) VALUES (?,?,?)' with params ["FR","France",null,"COUNTRY"]:
sqlSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'FR' for key 'PRIMARY')
Warning: Paris cannot be inserted in DB : reason(An exception occurred while executing 'INSERT INTO ss_locations (iso_code,"COUNTRY"]:
这是我尝试坚持它的方法:
// ...
foreach ( $locations as $location ) :
try {
DoctrineEntityManager::get()->persist($location);
DoctrineEntityManager::get()->flush();
} catch ( Throwable $e ) {}
}
// ...
解决方法
搜索后,我设法使用 EntityManager::merge( $entity );
后跟 EntityManager::flush();
调用使其工作,就像此处描述的 https://stackoverflow.com/a/46689619/8027308 一样。
// ...
foreach ( $locations as $location ) :
// Persist the main location
try {
DoctrineEntityManager::get()->merge($location);
DoctrineEntityManager::get()->flush();
} catch ( Throwable $e ) {}
endforeach;
// ...
但是,EntityManager::merge( $entity )
被标记为已弃用,将从 Doctrine 3+ 中删除。目前还没有官方替代方案,而且可能不会。
解决方法
1. EntityManager::getUnitOfWork()::registerManaged()
我尝试了此处提出的替代方案 https://stackoverflow.com/a/65050577/8027308,但在我的情况下使用 EntityManager::getUnitOfWork()::registerManaged()
不起作用并导致与 SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'FR' for key 'PRIMARY')
之前相同的错误。
此外,这种替代方案需要一两个以上的依赖项才能将实体数据转换为数组。这是产生错误的代码:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer;
// ...
$serializer = (new Serializer([new Normalizer\ObjectNormalizer()],[]));
foreach ( $locations as $location ) :
// Persist the main location
try {
DoctrineEntityManager::get()->getUnitOfWork()->registerManaged(
$location,// The entity object
[ $location->getIsoCode() ],// The entity identifiers
$serializer->normalize($location,null) // Gets the entity data as array
);
DoctrineEntityManager::get()->flush();
} catch ( Throwable $e ) {}
endforeach;
// ...
2.让 Doctrine 做一个预查询来检查父实体是否存在
否则,您可以在实体属性上禁用 cascade={"persist"}
并自行执行级联,使用 Doctrine 执行预查询以检查实体是否已存在于数据库中:
// ...
/**
* Recursively save all the parents into the DB
*
* @param ALocation $location The location to save the parents from
*
* @return void
*/
function persistParents( ALocation $location ) : void {
// If we don't have any parent,no need to go further
if ( ! $location->hasParent() )
return;
$parent = $location->getParent();
// Recursion to save all parents
if ($parent->hasParent())
persistParents($parent);
// Try to get the parent from the DB
$parentRecord = DoctrineEntityManager::get()->getRepository( ALocation::class )->find( $parent->getIsoCode() );
// If we succeed,we set the parent on the location and exit
if ( ! is_null($parentRecord) ) {
$location->setParent( $parentRecord );
return;
}
// Otherwise,we save it into the DB
try {
DoctrineEntityManager::get()->persist( $parent );
DoctrineEntityManager::get()->flush();
} catch (Throwable $e) {}
return;
}
foreach ( $locations as $location ) :
// Saves all the parents first
if ($location->hasParent())
persistParents( $location );
// Then persist the main location
try {
DoctrineEntityManager::get()->persist($location);
DoctrineEntityManager::get()->flush();
} catch ( Throwable $e ) {}
endforeach;
// ...
3.使用旧的 INSERT ... ON DUPLICATE KEY UPDATE
以前的解决方法只想使用 Doctrine,但更安全、更干净的解决方案是使用本机 SQL 查询,例如此处所述:https://stackoverflow.com/a/4205207/8027308
,问题不在于您的 Doctrine 配置,而在于您如何创建对象,如果您查看这些错误消息,您会发现 Doctrine 尝试为 France
和 Paris
插入相同的数据,请尝试将 $type
属性添加到没有学说映射到
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'FR' for key 'PRIMARY')
Warning: Île-de-France cannot be inserted in DB : reason(An exception occurred while executing 'INSERT INTO ss_locations (iso_code,name,parent_id,type) VALUES (?,?,?)' with params ["FR","France",null,"COUNTRY"]:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'FR' for key 'PRIMARY')
Warning: Paris cannot be inserted in DB : reason(An exception occurred while executing 'INSERT INTO ss_locations (iso_code,"COUNTRY"]: