diff --git a/docs/en/reference/working-with-objects.rst b/docs/en/reference/working-with-objects.rst index d88b814e8c4..cc889ddde3f 100644 --- a/docs/en/reference/working-with-objects.rst +++ b/docs/en/reference/working-with-objects.rst @@ -338,10 +338,11 @@ Performance of different deletion strategies Deleting an object with all its associated objects can be achieved in multiple ways with very different performance impacts. -1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM - will fetch this association. If its a Single association it will - pass this entity to - ``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``. +1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will + fetch this association. If it's a Single association it will pass + this entity to ``EntityManager#remove()``. If the association is a + collection, Doctrine will loop over all its elements and pass them to + ``EntityManager#remove()``. In both cases the cascade remove semantics are applied recursively. For large object graphs this removal strategy can be very costly. 2. Using a DQL ``DELETE`` statement allows you to delete multiple diff --git a/src/NativeQuery.php b/src/NativeQuery.php index aa44539d544..782983d50ec 100644 --- a/src/NativeQuery.php +++ b/src/NativeQuery.php @@ -50,7 +50,15 @@ protected function _doExecute() $types = []; foreach ($this->getParameters() as $parameter) { - $name = $parameter->getName(); + $name = $parameter->getName(); + + if ($parameter->typeWasSpecified()) { + $parameters[$name] = $parameter->getValue(); + $types[$name] = $parameter->getType(); + + continue; + } + $value = $this->processParameterValue($parameter->getValue()); $type = $parameter->getValue() === $value ? $parameter->getType() diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index 6769acca909..1e032e99b49 100644 --- a/src/Persisters/Collection/OneToManyPersister.php +++ b/src/Persisters/Collection/OneToManyPersister.php @@ -13,10 +13,13 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Utility\PersisterHelper; +use function array_fill; +use function array_keys; use function array_merge; use function array_reverse; use function array_values; use function assert; +use function count; use function implode; use function is_int; use function is_string; @@ -194,9 +197,12 @@ private function deleteEntityCollection(PersistentCollection $collection): int if ($targetClass->isInheritanceTypeSingleTable()) { $discriminatorColumn = $targetClass->getDiscriminatorColumn(); - $statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?'; - $parameters[] = $targetClass->discriminatorValue; - $types[] = $discriminatorColumn['type']; + $discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap); + $statement .= ' AND ' . $discriminatorColumn['name'] . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')'; + foreach ($discriminatorValues as $discriminatorValue) { + $parameters[] = $discriminatorValue; + $types[] = $discriminatorColumn['type']; + } } $numAffected = $this->conn->executeStatement($statement, $parameters, $types); diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 00fe7b03703..5ca00cb007e 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -832,17 +832,42 @@ public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifie $computedIdentifier = []; + /** @var array|null $sourceEntityData */ + $sourceEntityData = null; + // TRICKY: since the association is specular source and target are flipped foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) { - throw MappingException::joinColumnMustPointToMappedField( - $sourceClass->name, - $sourceKeyColumn - ); - } + // The likely case here is that the column is a join column + // in an association mapping. However, there is no guarantee + // at this point that a corresponding (generally identifying) + // association has been mapped in the source entity. To handle + // this case we directly reference the column-keyed data used + // to initialize the source entity before throwing an exception. + $resolvedSourceData = false; + if (! isset($sourceEntityData)) { + $sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity); + } + + if (isset($sourceEntityData[$sourceKeyColumn])) { + $dataValue = $sourceEntityData[$sourceKeyColumn]; + if ($dataValue !== null) { + $resolvedSourceData = true; + $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = + $dataValue; + } + } - $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = - $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + if (! $resolvedSourceData) { + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, + $sourceKeyColumn + ); + } + } else { + $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = + $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } } $targetEntity = $this->load($computedIdentifier, null, $assoc); diff --git a/tests/Tests/Models/OneToOneInverseSideWithAssociativeIdLoad/InverseSide.php b/tests/Tests/Models/OneToOneInverseSideWithAssociativeIdLoad/InverseSide.php new file mode 100644 index 00000000000..0dcb9a93a1b --- /dev/null +++ b/tests/Tests/Models/OneToOneInverseSideWithAssociativeIdLoad/InverseSide.php @@ -0,0 +1,34 @@ +createSchemaForModels(OwningSide::class, InverseSideIdTarget::class, InverseSide::class); + } + + /** @group GH-11108 */ + public function testInverseSideWithAssociativeIdOneToOneLoadedAfterDqlQuery(): void + { + $owner = new OwningSide(); + $inverseId = new InverseSideIdTarget(); + $inverse = new InverseSide(); + + $owner->id = 'owner'; + $inverseId->id = 'inverseId'; + $inverseId->inverseSide = $inverse; + $inverse->associativeId = $inverseId; + $owner->inverse = $inverse; + $inverse->owning = $owner; + + $this->_em->persist($owner); + $this->_em->persist($inverseId); + $this->_em->persist($inverse); + $this->_em->flush(); + $this->_em->clear(); + + $fetchedInverse = $this + ->_em + ->createQueryBuilder() + ->select('inverse') + ->from(InverseSide::class, 'inverse') + ->andWhere('inverse.associativeId = :associativeId') + ->setParameter('associativeId', 'inverseId') + ->getQuery() + ->getSingleResult(); + assert($fetchedInverse instanceof InverseSide); + + self::assertInstanceOf(InverseSide::class, $fetchedInverse); + self::assertInstanceOf(InverseSideIdTarget::class, $fetchedInverse->associativeId); + self::assertInstanceOf(OwningSide::class, $fetchedInverse->owning); + + $this->assertSQLEquals( + 'select o0_.associativeid as associativeid_0 from one_to_one_inverse_side_assoc_id_load_inverse o0_ where o0_.associativeid = ?', + $this->getLastLoggedQuery(1)['sql'] + ); + + $this->assertSQLEquals( + 'select t0.id as id_1, t0.inverse as inverse_2 from one_to_one_inverse_side_assoc_id_load_owning t0 where t0.inverse = ?', + $this->getLastLoggedQuery()['sql'] + ); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH11501Test.php b/tests/Tests/ORM/Functional/Ticket/GH11501Test.php new file mode 100644 index 00000000000..715137d43af --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11501Test.php @@ -0,0 +1,120 @@ +setUpEntitySchema([ + GH11501AbstractTestEntity::class, + GH11501TestEntityOne::class, + GH11501TestEntityTwo::class, + GH11501TestEntityHolder::class, + ]); + } + + /** + * @throws ORMException + */ + public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void + { + $testEntityOne = new GH11501TestEntityOne(); + $testEntityTwo = new GH11501TestEntityTwo(); + $testEntityHolder = new GH11501TestEntityHolder(); + + $testEntityOne->testEntityHolder = $testEntityHolder; + $testEntityHolder->testEntities->add($testEntityOne); + + $testEntityTwo->testEntityHolder = $testEntityHolder; + $testEntityHolder->testEntities->add($testEntityTwo); + + $em = $this->getEntityManager(); + $em->persist($testEntityOne); + $em->persist($testEntityTwo); + $em->persist($testEntityHolder); + $em->flush(); + + $testEntityHolder->testEntities = new ArrayCollection(); + $em->persist($testEntityHolder); + $em->flush(); + $em->refresh($testEntityHolder); + + static::assertEmpty($testEntityHolder->testEntities->toArray(), 'All records should have been deleted'); + } +} + + + +/** + * @ORM\Entity + * @ORM\Table(name="one_to_many_single_table_inheritance_test_entities_parent_join") + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorColumn(name="type", type="string") + * @ORM\DiscriminatorMap({"test_entity_one"="GH11501TestEntityOne", "test_entity_two"="GH11501TestEntityTwo"}) + */ +class GH11501AbstractTestEntity +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity="GH11501TestEntityHolder", inversedBy="testEntities") + * @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id") + * + * @var GH11501TestEntityHolder + */ + public $testEntityHolder; +} + + +/** @ORM\Entity */ +class GH11501TestEntityOne extends GH11501AbstractTestEntity +{ +} + +/** @ORM\Entity */ +class GH11501TestEntityTwo extends GH11501AbstractTestEntity +{ +} + +/** @ORM\Entity */ +class GH11501TestEntityHolder +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * + * @var int + */ + public $id; + + /** + * @ORM\OneToMany(targetEntity="GH11501AbstractTestEntity", mappedBy="testEntityHolder", orphanRemoval=true) + * + * @var Collection + */ + public $testEntities; + + public function __construct() + { + $this->testEntities = new ArrayCollection(); + } +} diff --git a/tests/Tests/ORM/Query/NativeQueryTest.php b/tests/Tests/ORM/Query/NativeQueryTest.php new file mode 100644 index 00000000000..0e68389494e --- /dev/null +++ b/tests/Tests/ORM/Query/NativeQueryTest.php @@ -0,0 +1,42 @@ +entityManager = $this->getTestEntityManager(); + } + + public function testValuesAreNotBeingResolvedForSpecifiedParameterTypes(): void + { + $unitOfWork = $this->createMock(UnitOfWork::class); + + $this->entityManager->setUnitOfWork($unitOfWork); + + $unitOfWork + ->expects(self::never()) + ->method('getSingleIdentifierValue'); + + $rsm = new ResultSetMapping(); + + $query = $this->entityManager->createNativeQuery('SELECT d.* FROM date_time_model d WHERE d.datetime = :value', $rsm); + + $query->setParameter('value', new DateTime(), Types::DATETIME_MUTABLE); + + self::assertEmpty($query->getResult()); + } +}