From e3d7c6076c3975eedd1abb8f2b2eeb34efcd7b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 26 Jun 2024 17:45:09 +0200 Subject: [PATCH 1/6] Use modern array syntax in the doc --- docs/en/tutorials/getting-started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 688030cc149..1fa463dd797 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -144,7 +144,7 @@ step: // Create a simple "default" Doctrine ORM configuration for Attributes $config = ORMSetup::createAttributeMetadataConfiguration( - paths: array(__DIR__."/src"), + paths: [__DIR__ . '/src'], isDevMode: true, ); // or if you prefer annotation, YAML or XML @@ -153,7 +153,7 @@ step: // isDevMode: true, // ); // $config = ORMSetup::createXMLMetadataConfiguration( - // paths: array(__DIR__."/config/xml"), + // paths: [__DIR__ . '/config/xml'], // isDevMode: true, //); // $config = ORMSetup::createYAMLMetadataConfiguration( From 19129e9f8a012c0789d17ef72c263ea384bbc48b Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Fri, 28 Jun 2024 08:02:28 +0200 Subject: [PATCH 2/6] working-with-objects.rst: added missing white space --- docs/en/reference/working-with-objects.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 From 9bd51aaeb6d0f61f4d25ea838a951cb52db1e8b7 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 3 Jul 2024 15:14:49 +0200 Subject: [PATCH 3/6] Fix the support for custom parameter types in native queries The Query class (used for DQL queries) takes care of using the value and type as is when a type was specified for a parameter instead of going through the default processing of values. The NativeQuery class was missing the equivalent check, making the custom type work only if the default processing of values does not convert the value to a different one. --- src/NativeQuery.php | 10 +++++- tests/Tests/ORM/Query/NativeQueryTest.php | 42 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/Tests/ORM/Query/NativeQueryTest.php 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/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()); + } +} From 121158f92c9e5c5ed52accafeaaca8e3412cf9ec Mon Sep 17 00:00:00 2001 From: Kyron Taylor Date: Sat, 3 Aug 2024 16:49:18 +0100 Subject: [PATCH 4/6] GH11551 - fix OneToManyPersister::deleteEntityCollection when using single-inheritence entity parent as targetEntity. When using the parent entity for a single-inheritence table as the targetEntity for a property, the discriminator value should be all of the values in the discriminator map. OneToManyPersister::deleteEntityCollection has been amended to reflect this. --- .../Collection/OneToManyPersister.php | 12 +- .../ORM/Functional/Ticket/GH11501Test.php | 120 ++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH11501Test.php diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index 6769acca909..aed37556bc7 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/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(); + } +} From 2707b09a07e00097bfef81f3ee35c6435586e0a6 Mon Sep 17 00:00:00 2001 From: gitbugr Date: Sat, 3 Aug 2024 21:38:49 +0100 Subject: [PATCH 5/6] fix spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Paris --- src/Persisters/Collection/OneToManyPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index aed37556bc7..1e032e99b49 100644 --- a/src/Persisters/Collection/OneToManyPersister.php +++ b/src/Persisters/Collection/OneToManyPersister.php @@ -201,7 +201,7 @@ private function deleteEntityCollection(PersistentCollection $collection): int $statement .= ' AND ' . $discriminatorColumn['name'] . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')'; foreach ($discriminatorValues as $discriminatorValue) { $parameters[] = $discriminatorValue; - $types[] = $discriminatorColumn['type']; + $types[] = $discriminatorColumn['type']; } } From fe4a2e83cff3404241e85796197ea7d411e09ba1 Mon Sep 17 00:00:00 2001 From: Matthew Curland Date: Fri, 16 Aug 2024 18:49:21 -0600 Subject: [PATCH 6/6] Original entity data resolves inverse 1-1 joins If the source entity for an inverse (non-owning) 1-1 relationship is identified by an association then the identifying association may not be set when an inverse one-to-one association is resolved. This means that no data is available in the entity to resolve the needed column value for the join query. The original entity data can be retrieved from the unit of work and is used as a fallback to populate the query condition. Fixes #11108 --- .../Entity/BasicEntityPersister.php | 39 +++++++++-- .../InverseSide.php | 34 ++++++++++ .../InverseSideIdTarget.php | 33 +++++++++ .../OwningSide.php | 37 ++++++++++ ...WithAssociativeIdLoadAfterDqlQueryTest.php | 68 +++++++++++++++++++ 5 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 tests/Tests/Models/OneToOneInverseSideWithAssociativeIdLoad/InverseSide.php create mode 100644 tests/Tests/Models/OneToOneInverseSideWithAssociativeIdLoad/InverseSideIdTarget.php create mode 100644 tests/Tests/Models/OneToOneInverseSideWithAssociativeIdLoad/OwningSide.php create mode 100644 tests/Tests/ORM/Functional/OneToOneInverseSideWithAssociativeIdLoadAfterDqlQueryTest.php 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'] + ); + } +}