Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot persist new entity if primary key contains a foreign key and the referenced object is in state new and its id is not assigned #7003

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion lib/Doctrine/ORM/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,18 @@ private function persistNew($class, $entity)
$class->setIdentifierValues($entity, $idValue);
}

$this->entityIdentifiers[$oid] = $idValue;
// Some identifiers may be foreign keys to new entities.
// In this case, we don't have the value yet and should treat it as if we have a post-insert generator
$hasMissingIdsWhichAreForeignKeys = false;
foreach($idValue as $idField => $idFieldValue) {
if($idFieldValue === null && isset($class->associationMappings[$idField])) {
$hasMissingIdsWhichAreForeignKeys = true;
break;
}
}
if(!$hasMissingIdsWhichAreForeignKeys) {
$this->entityIdentifiers[$oid] = $idValue;
}
}

$this->entityStates[$oid] = self::STATE_MANAGED;
Expand Down Expand Up @@ -1033,12 +1044,16 @@ private function executeInserts($class)
$persister = $this->getEntityPersister($className);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);

$theseInsertions = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$theseInsertions is a bad name here


foreach ($this->entityInsertions as $oid => $entity) {

if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}

$theseInsertions[$oid] = $entity;

$persister->addInsert($entity);

unset($this->entityInsertions[$oid]);
Expand Down Expand Up @@ -1067,6 +1082,34 @@ private function executeInserts($class)

$this->addToIdentityMap($entity);
}
} else {
foreach ($theseInsertions as $oid => $entity) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the commit order calculator is working correctly, then this should simply be pushed down in the execution path, no?

if(!isset($this->entityIdentifiers[$oid])) {
//entity was not added to identity map because some identifiers are foreign keys to new entities.
//add it now
$identifier = [];
$idFields = $class->getIdentifierFieldNames();
foreach ($idFields as $idField) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block seems to detect whether an identifier is incomplete. Can it be moved to a private method?

$value = $class->getFieldValue($entity, $idField);

if (isset($class->associationMappings[$idField])) {
// NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
$value = $this->getSingleIdentifierValue($value);
}

$identifier[$idField] = $value;
}

$this->entityIdentifiers[$oid] = $identifier;

foreach($identifier as $idField => $idValue) {
$this->originalEntityData[$oid][$idField] = $idValue;
}

$this->entityStates[$oid] = self::STATE_MANAGED;
$this->addToIdentityMap($entity);
}
}
}

foreach ($entities as $entity) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Doctrine\Tests\Models\IdentityThroughForeignKeyTest;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
* Class Product
* @package Doctrine\Tests\Models\IdentityThroughForeignKeyTest
* @Entity()
* @Table(name="identitythroughforeignkey_product")
*/
class Product
{

/**
* @var integer
* @Id()
* @GeneratedValue(strategy="IDENTITY")
* @Column(name="id")
*/
public $id;

/**
* @var Collection|ProductColor[]
* @OneToMany(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductColor", mappedBy="product", cascade={"persist"})
*/
public $colors;

/**
* @var Collection|ProductColor[]
* @OneToMany(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductSize", mappedBy="product", cascade={"persist"})
*/
public $sizes;

/**
* @var Collection|ProductVariant[]
* @OneToMany(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductVariant", mappedBy="product", cascade={"persist"})
*/
public $variants;

public function __construct()
{
$this->colors = new ArrayCollection();
$this->sizes = new ArrayCollection();
$this->variants = new ArrayCollection();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Doctrine\Tests\Models\IdentityThroughForeignKeyTest;

/**
* Class ProductColor
* @package Doctrine\Tests\Models\IdentityThroughForeignKeyTest
* @Entity()
* @Table(name="identitythroughforeignkey_product_color")
*/
class ProductColor
{

/**
* @var integer
* @Id()
* @GeneratedValue(strategy="IDENTITY")
* @Column(name="id", type="integer")
*/
public $id;

/**
* @var Product
* @ManyToOne(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\Product", inversedBy="colors")
* @JoinColumn(name="product_id", referencedColumnName="id")
*/
public $product;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Doctrine\Tests\Models\IdentityThroughForeignKeyTest;

use Doctrine\ORM\Mapping as ORM;

/**
* Class ProductSize
* @package Doctrine\Tests\Models\IdentityThroughForeignKeyTest
* @Entity()
* @Table(name="identitythroughforeignkey_product_size")
*/
class ProductSize
{

/**
* @var integer
* @Id()
* @GeneratedValue(strategy="IDENTITY")
* @Column(name="id", type="integer")
*/
public $id;

/**
* @var Product
* @ManyToOne(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\Product", inversedBy="sizes")
* @JoinColumn(name="product_id", referencedColumnName="id")
*/
public $product;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Doctrine\Tests\Models\IdentityThroughForeignKeyTest;

/**
* Class ProductVariant
* @package Doctrine\Tests\Models\IdentityThroughForeignKeyTest
* @Entity()
* @Table(name="identitythroughforeignkey_product_variant")
*/
class ProductVariant
{

/**
* @var Product
* @Id()
* @ManyToOne(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\Product", inversedBy="variants")
* @JoinColumn(name="product_id", referencedColumnName="id")
*/
public $product;

/**
* @var ProductColor
* @Id()
* @ManyToOne(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductColor")
* @JoinColumn(name="color_id", referencedColumnName="id")
*/
public $color;

/**
* @var ProductSize
* @Id()
* @ManyToOne(targetEntity="Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductSize")
* @JoinColumn(name="size_id", referencedColumnName="id")
*/
public $size;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Doctrine\Tests\ORM\Functional;

use Doctrine\ORM\EntityManager;
use Doctrine\Tests\Mocks\ConnectionMock;
use Doctrine\Tests\Models\IdentityThroughForeignKeyTest\Product;
use Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductColor;
use Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductSize;
use Doctrine\Tests\Models\IdentityThroughForeignKeyTest\ProductVariant;
use Doctrine\Tests\OrmTestCase;

class IdentityThroughForeignKeyTest extends OrmTestCase
{

/** @var EntityManager */
protected $em;

protected function setUp()
{
parent::setUp();
$this->em = $this->_getTestEntityManager();
}


public function testIdentityThroughForeignKeyCollectionPersistence()
{
$product = new Product();

$color = new ProductColor();
$color->product = $product;
$product->colors->add($color);

$size = new ProductSize();
$size->product = $product;
$product->sizes->add($size);

$variant = new ProductVariant();
$variant->product = $product;
$variant->color = $color;
$variant->size = $size;
$product->variants->add($variant);

$this->em->persist($product);

//check that flush does not throw
$this->em->flush();

$identifier = $this->em->getClassMetadata(ProductVariant::class)->getIdentifierValues($variant);

foreach ($identifier as $k => $v) {
$identifier[$k] = $this->em->getClassMetadata(get_class($v))->getIdentifierValues($v)['id'];
}

//check that identityMap content is correct
//find by primary key should return the same instance
$this->assertTrue($variant === $this->em->find(ProductVariant::class, $identifier));
}
}