Doctrine ORM self ManyToOne 由于重复条目而无法插入

Posted

技术标签:

【中文标题】Doctrine ORM self ManyToOne 由于重复条目而无法插入【英文标题】:Doctrine ORM self ManyToOne fails to insert because of duplicate entries 【发布时间】:2021-06-29 18:58:36 【问题描述】:

问题

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

说明

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

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

/**
 * 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, name, parent_id, type) VALUES (?, ?, ?, ?)' with params ["FR", "France", null, "COUNTRY"]:

这是所需数据库内容的示例

这是我尝试坚持的方式:

// ...

  foreach ( $locations as $location ) :
    try 
      DoctrineEntityManager::get()->persist($location);
      DoctrineEntityManager::get()->flush();
     catch ( Throwable $e ) 
  

// ...

【问题讨论】:

【参考方案1】:

搜索后,我设法使用EntityManager::merge( $entity );,然后调用EntityManager::flush(); 使其工作,就像这里描述的https://***.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://***.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 执行预查询以检查实体是否已存在于 DB 中:

// ...

/**
 * 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://***.com/a/4205207/8027308

【讨论】:

【参考方案2】:

问题不在于您的 Doctrine 配置,而在于您如何创建对象,如果您查看这些错误消息,您会发现 Doctrine 尝试为 FranceParis 插入相同的数据,尝试添加 @987654323 @属性没有学说映射到

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, name, parent_id, type) VALUES (?, ?, ?, ?)' with params ["FR", "France", null, "COUNTRY"]:

更新

正如回复中所讨论的,主要问题来自于构造实体的方式,解决方案是为父实体创建唯一的对象,然后将它们附加到所有子实体(使用引用)

$france = new CountryLocation('France');
$iledefrance = new DepartmentLocation('Ile-de-france');
$iledefrance->setParent($france);
$paris = new CityLocation('Paris');
$paris->setParent($iledefrance);
....

【讨论】:

每个子类将TYPE const 重新定义为"COUNTRY""REGION""DEPARTMENT",并被Doctrine 用作鉴别器列,我试图将其转换为@987654330 @ 而不是 const 但效果不佳:php /** * A type that determines the location type */ static ?string $type = null; 我不确定学说在区分映射中使用 const,尝试声明一个简单的属性 protect $type 并声明它的 setter 和 getter,以确保一切正常,您可以在刷新之前转储实体实体管理器 好的,所以我尝试使用 protect $type 非静态属性,但效果不佳...我将通过修改更新我的原始帖子 最新! :) 您可以重构实体的创建,对于 Doctrine,如果您为 Paris 和 Ile-de-France Doctrine 创建一个新的 Country 对象,则每个对象都是基于对象本身的哈希在内部进行管理的尝试将法国持久化两次,因为它们是两个不同的 PHP 对象,您可以尝试以先创建所有父对象的方式准备实体$locations,然后将它们($children->setParent($parent))附加到子对象(国家 -> 地区 -> 部门) , 在这种情况下不需要每次都刷新

以上是关于Doctrine ORM self ManyToOne 由于重复条目而无法插入的主要内容,如果未能解决你的问题,请参考以下文章

PHP Doctrine 初学者:找不到 Doctrine\ORM\Tools\Setup

Doctrine Abstract - 具体类 ORM

Doctrine异常 - [Doctrine ORM Mapping MappingException]

Doctrine 2 ORM 级联删除相关实体

Doctrine 2.2 ORM:查找扩展实体

类中的教义注释“@Doctrine\ORM\Annotation\Entity”不存在或无法自动加载