由于重复条目,Doctrine ORM self ManyToOne 无法插入 1. EntityManager::getUnitOfWork()::registerManaged()2.让 Doctrine 做一个预查询来检查父实体是否存在3.使用旧的 INSERT ... ON DUPLICATE KEY UPDATE

问题描述

问题

当 Doctrine 在 cascade={"persist"} 关系上使用 ManyToOne 持久化时,有没有一种方法可以识别现有对象,并且在尝试再次插入它时不会失败从而违反唯一键规则?

>

说明

我正在尝试创建一个可以引用父级的位置实体来获取这种结构:

enter image description here

为此,我的实体上有以下代码

/**
 * 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"]:

这是想要的数据库内容的示例

enter image description here

这是我尝试坚持它的方法

// ...

  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 尝试为 FranceParis 插入相同的数据,请尝试将 $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"]: