Skip to content

Commit

Permalink
Fix @SequenceGeneratorDefinition inheritance, take 1 (#11050)
Browse files Browse the repository at this point in the history
#10927 reported that #10455 broke the way how the default `@SequenceGeneratorDefinition` is created and inherited by subclasses for ID columns using `@GeneratedValue(strategy="SEQUENCE")`.

First, I had to understand how `@SequenceGeneratorDefinition` has been handled before #10455 when entity inheritance comes into play:

* Entity and mapped superclasses inherit the ID generator type (as given by `@GeneratedValue`) from their parent classes
* `@SequenceGeneratorDefinition`, however, is not generally inherited
* ... instead, a default sequence generator definition is created for every class when no explicit configuration is given. In this case, sequence names are based on the current class' table name.
* Once a root entity has been identified, all subclasses inherit its sequence generator definition unchanged.

#### Why did #10455 break this?

When I implemented #10455, I was mislead by two tests `BasicInheritanceMappingTest::testGeneratedValueFromMappedSuperclass` and `BasicInheritanceMappingTest::testMultipleMappedSuperclasses`.

These tests check the sequence generator definition that is inherited by an entity class from a mapped superclass, either directly or through an additional (intermediate) mapped superclass.

The tests expect the sequence generator definition on the entity _to be the same_ as on the base mapped superclass.

The reason why the tests worked before was the quirky behaviour of the annotation and attribute drivers that #10455 was aiming at: The drivers did not report the `@SequenceGeneratorDefinition` on the base mapped superclass where it was actually defined. Instead, they reported this `@SequenceGeneratorDefinition` for the entity class only.

This means the inheritance rules stated above did not take effect, since the ID field with the sequence generator was virtually pushed down to the entity class.

In #10455, I did not realize that these failing tests had to do with the quirky and changed mapping driver behaviour. Instead, I tried to "fix" the inheritance rules by passing along the sequence generator definition unchanged once the ID column had been defined.

#### Consequences of the change suggested here

This PR reverts the changes made to `@SequenceGeneratorDefinition` inheritance behaviour that were done in #10455.

This means that with the new "report fields where declared" driver mode (which is active in our functional tests) we can not expect the sequence generator definition to be inherited from mapped superclasses. The two test cases from `BasicInheritanceMappingTest` are removed.

I will leave a notice in #10455 to indicate that the new driver mode also affects sequence generator definitions.

The `GH10927Test` test case validates the sequence names generated in a few cases. In fact, I wrote this test against the `2.15.x` branch to make sure we get results that are consistent with the previous behaviour.

This also means `@SequenceGeneratorDefinition` on mapped superclasses is pointless: The mapped superclass does not make use of the definition itself (it has no table), and the setting is never inherited to child classes.
 
Fixes #10927. There is another implementation with slightly different inheritance semantics in #11052, in case the fix is not good enough and we'd need to review the topic later on.
  • Loading branch information
mpdude authored Jan 12, 2024
1 parent c6b3509 commit 3dd3d38
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 35 deletions.
10 changes: 7 additions & 3 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
if ($parent) {
$class->setInheritanceType($parent->inheritanceType);
$class->setDiscriminatorColumn($parent->discriminatorColumn);
$this->inheritIdGeneratorMapping($class, $parent);
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
Expand Down Expand Up @@ -151,8 +151,12 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
throw MappingException::reflectionFailure($class->getName(), $e);
}

// Complete id generator mapping when the generator was declared/added in this class
if ($class->identifier && (! $parent || ! $parent->identifier)) {
// If this class has a parent the id generator strategy is inherited.
// However this is only true if the hierarchy of parents contains the root entity,
// if it consists of mapped superclasses these don't necessarily include the id field.
if ($parent && $rootEntityFound) {
$this->inheritIdGeneratorMapping($class, $parent);
} else {
$this->completeIdGeneratorMapping($class);
}

Expand Down
122 changes: 122 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH10927Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

/**
* @group GH-10927
*/
class GH10927Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$platform = $this->_em->getConnection()->getDatabasePlatform();
if (! $platform instanceof PostgreSQLPlatform) {
self::markTestSkipped('The ' . self::class . ' requires the use of postgresql.');
}

$this->setUpEntitySchema([
GH10927RootMappedSuperclass::class,
GH10927InheritedMappedSuperclass::class,
GH10927EntityA::class,
GH10927EntityB::class,
GH10927EntityC::class,
]);
}

public function testSequenceGeneratorDefinitionForRootMappedSuperclass(): void
{
$metadata = $this->_em->getClassMetadata(GH10927RootMappedSuperclass::class);

self::assertNull($metadata->sequenceGeneratorDefinition);
}

public function testSequenceGeneratorDefinitionForEntityA(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityA::class);

self::assertSame('GH10927EntityA_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}

public function testSequenceGeneratorDefinitionForInheritedMappedSuperclass(): void
{
$metadata = $this->_em->getClassMetadata(GH10927InheritedMappedSuperclass::class);

self::assertSame('GH10927InheritedMappedSuperclass_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}

public function testSequenceGeneratorDefinitionForEntityB(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityB::class);

self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}

public function testSequenceGeneratorDefinitionForEntityC(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityC::class);

self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}
}

/**
* @ORM\MappedSuperclass()
*/
class GH10927RootMappedSuperclass
{
}

/**
* @ORM\Entity()
*/
class GH10927EntityA extends GH10927RootMappedSuperclass
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\Column(type="integer")
*
* @var int|null
*/
private $id = null;
}

/**
* @ORM\MappedSuperclass()
*/
class GH10927InheritedMappedSuperclass extends GH10927RootMappedSuperclass
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\Column(type="integer")
*
* @var int|null
*/
private $id = null;
}

/**
* @ORM\Entity()
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({"B" = "GH10927EntityB", "C" = "GH10927EntityC"})
*/
class GH10927EntityB extends GH10927InheritedMappedSuperclass
{
}

/**
* @ORM\Entity()
*/
class GH10927EntityC extends GH10927EntityB
{
}
33 changes: 1 addition & 32 deletions tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,22 +163,7 @@ public function testMappedSuperclassWithId(): void
/**
* @group DDC-1156
* @group DDC-1218
*/
public function testGeneratedValueFromMappedSuperclass(): void
{
$class = $this->cmf->getMetadataFor(SuperclassEntity::class);
assert($class instanceof ClassMetadata);

self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
$class->sequenceGeneratorDefinition
);
}

/**
* @group DDC-1156
* @group DDC-1218
* @group GH-10927
*/
public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass(): void
{
Expand All @@ -192,22 +177,6 @@ public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass():
);
}

/**
* @group DDC-1156
* @group DDC-1218
*/
public function testMultipleMappedSuperclasses(): void
{
$class = $this->cmf->getMetadataFor(MediumSuperclassEntity::class);
assert($class instanceof ClassMetadata);

self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
$class->sequenceGeneratorDefinition
);
}

/**
* Ensure indexes are inherited from the mapped superclass.
*
Expand Down

0 comments on commit 3dd3d38

Please sign in to comment.