Skip to content

Commit

Permalink
Merge pull request #10598 from opsway/fix-generated-for-joined-inheri…
Browse files Browse the repository at this point in the history
…tance

Support not Insertable/Updateable columns for entities with `JOINED` inheritance type
  • Loading branch information
greg0ire authored Jul 11, 2023
2 parents 8c513a6 + 3b3056f commit b5987ad
Show file tree
Hide file tree
Showing 10 changed files with 528 additions and 6 deletions.
4 changes: 4 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2764,6 +2764,10 @@ public function addInheritedFieldMapping(array $fieldMapping)
$this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
$this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
$this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName'];

if (isset($fieldMapping['generated'])) {
$this->requiresFetchAfterChange = true;
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class BasicEntityPersister implements EntityPersister
*
* @var IdentifierFlattener
*/
private $identifierFlattener;
protected $identifierFlattener;

/** @var CachedPersisterContext */
protected $currentPersisterContext;
Expand Down Expand Up @@ -379,7 +379,7 @@ protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id
* @return int[]|null[]|string[]
* @psalm-return list<int|string|null>
*/
private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
{
$types = [];

Expand Down
66 changes: 62 additions & 4 deletions lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
use Doctrine\ORM\Internal\SQLResultCasing;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Utility\PersisterHelper;
use LengthException;

use function array_combine;
use function array_keys;
use function array_values;
use function implode;

/**
Expand Down Expand Up @@ -165,10 +168,6 @@ public function executeInserts()
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
}

if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}

// Execute inserts on subtables.
// The order doesn't matter because all child tables link to the root table via FK.
foreach ($subTableStmts as $tableName => $stmt) {
Expand All @@ -189,6 +188,10 @@ public function executeInserts()

$stmt->executeStatement();
}

if ($this->class->requiresFetchAfterChange) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}

$this->queuedInserts = [];
Expand Down Expand Up @@ -510,6 +513,7 @@ protected function getInsertColumnList()
|| isset($this->class->associationMappings[$name]['inherited'])
|| ($this->class->isVersioned && $this->class->versionField === $name)
|| isset($this->class->embeddedClasses[$name])
|| isset($this->class->fieldMappings[$name]['notInsertable'])
) {
continue;
}
Expand Down Expand Up @@ -552,6 +556,60 @@ protected function assignDefaultVersionAndUpsertableValues($entity, array $id)
}
}

/**
* {@inheritDoc}
*/
protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
{
$columnNames = [];
foreach ($this->class->fieldMappings as $key => $column) {
$class = null;
if ($this->class->isVersioned && $key === $versionedClass->versionField) {
$class = $versionedClass;
} elseif (isset($column['generated'])) {
$class = isset($column['inherited'])
? $this->em->getClassMetadata($column['inherited'])
: $this->class;
} else {
continue;
}

$columnNames[$key] = $this->getSelectColumnSQL($key, $class);
}

$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
$baseTableAlias = $this->getSQLTableAlias($this->class->name);
$joinSql = $this->getJoinSql($baseTableAlias);
$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
foreach ($identifier as $i => $idValue) {
$identifier[$i] = $baseTableAlias . '.' . $idValue;
}

$sql = 'SELECT ' . implode(', ', $columnNames)
. ' FROM ' . $tableName . ' ' . $baseTableAlias
. $joinSql
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';

$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
$values = $this->conn->fetchNumeric(
$sql,
array_values($flatId),
$this->extractIdentifierTypes($id, $versionedClass)
);

if ($values === false) {
throw new LengthException('Unexpected empty result for database query.');
}

$values = array_combine(array_keys($columnNames), $values);

if (! $values) {
throw new LengthException('Unexpected number of database columns.');
}

return $values;
}

private function getJoinSql(string $baseTableAlias): string
{
$joinSql = '';
Expand Down
261 changes: 261 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH9467/GH9467Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket\GH9467;

use Doctrine\Tests\OrmFunctionalTestCase;

class GH9467Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(
JoinedInheritanceRoot::class,
JoinedInheritanceChild::class,
JoinedInheritanceWritableColumn::class,
JoinedInheritanceNonWritableColumn::class,
JoinedInheritanceNonInsertableColumn::class,
JoinedInheritanceNonUpdatableColumn::class
);
}

public function testRootColumnsInsert(): int
{
$entity = new JoinedInheritanceChild();
$entity->rootWritableContent = 'foo';
$entity->rootNonWritableContent = 'foo';
$entity->rootNonInsertableContent = 'foo';
$entity->rootNonUpdatableContent = 'foo';

$this->_em->persist($entity);
$this->_em->flush();

// check INSERT query cause set database values into non-insertable entity properties
self::assertEquals('foo', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('dbDefault', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceChild::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceChild::class, $entity);
self::assertEquals('foo', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('dbDefault', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);

return $entity->id;
}

/** @depends testRootColumnsInsert */
public function testRootColumnsUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceChild::class, $entityId);
self::assertInstanceOf(JoinedInheritanceChild::class, $entity);

// update exist entity
$entity->rootWritableContent = 'bar';
$entity->rootNonInsertableContent = 'bar';
$entity->rootNonWritableContent = 'bar';
$entity->rootNonUpdatableContent = 'bar';

$this->_em->persist($entity);
$this->_em->flush();

// check UPDATE query cause set database values into non-insertable entity properties
self::assertEquals('bar', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('bar', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceChild::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceChild::class, $entity);
self::assertEquals('bar', $entity->rootWritableContent);
self::assertEquals('dbDefault', $entity->rootNonWritableContent);
self::assertEquals('bar', $entity->rootNonInsertableContent);
self::assertEquals('foo', $entity->rootNonUpdatableContent);
}

public function testChildWritableColumnInsert(): int
{
$entity = new JoinedInheritanceWritableColumn();
$entity->writableContent = 'foo';

$this->_em->persist($entity);
$this->_em->flush();

// check INSERT query doesn't change insertable entity property
self::assertEquals('foo', $entity->writableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceWritableColumn::class, $entity);
self::assertEquals('foo', $entity->writableContent);

return $entity->id;
}

/** @depends testChildWritableColumnInsert */
public function testChildWritableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceWritableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceWritableColumn::class, $entity);

// update exist entity
$entity->writableContent = 'bar';

$this->_em->persist($entity);
$this->_em->flush();

// check UPDATE query doesn't change updatable entity property
self::assertEquals('bar', $entity->writableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceWritableColumn::class, $entity);
self::assertEquals('bar', $entity->writableContent);
}

public function testChildNonWritableColumnInsert(): int
{
$entity = new JoinedInheritanceNonWritableColumn();
$entity->nonWritableContent = 'foo';

$this->_em->persist($entity);
$this->_em->flush();

// check INSERT query cause set database value into non-insertable entity property
self::assertEquals('dbDefault', $entity->nonWritableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonWritableColumn::class, $entity);
self::assertEquals('dbDefault', $entity->nonWritableContent);

return $entity->id;
}

/** @depends testChildNonWritableColumnInsert */
public function testChildNonWritableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceNonWritableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceNonWritableColumn::class, $entity);

// update exist entity
$entity->nonWritableContent = 'bar';
// change some property to ensure UPDATE query will be done
self::assertNotEquals('bar', $entity->rootField);
$entity->rootField = 'bar';

$this->_em->persist($entity);
$this->_em->flush();

// check UPDATE query cause set database value into non-updatable entity property
self::assertEquals('dbDefault', $entity->nonWritableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonWritableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonWritableColumn::class, $entity);
self::assertEquals('bar', $entity->rootField); // check that UPDATE query done
self::assertEquals('dbDefault', $entity->nonWritableContent);
}

public function testChildNonInsertableColumnInsert(): int
{
$entity = new JoinedInheritanceNonInsertableColumn();
$entity->nonInsertableContent = 'foo';

$this->_em->persist($entity);
$this->_em->flush();

// check INSERT query cause set database value into non-insertable entity property
self::assertEquals('dbDefault', $entity->nonInsertableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonInsertableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonInsertableColumn::class, $entity);
self::assertEquals('dbDefault', $entity->nonInsertableContent);

return $entity->id;
}

/** @depends testChildNonInsertableColumnInsert */
public function testChildNonInsertableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceNonInsertableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceNonInsertableColumn::class, $entity);

// update exist entity
$entity->nonInsertableContent = 'bar';

$this->_em->persist($entity);
$this->_em->flush();

// check UPDATE query doesn't change updatable entity property
self::assertEquals('bar', $entity->nonInsertableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonInsertableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonInsertableColumn::class, $entity);
self::assertEquals('bar', $entity->nonInsertableContent);
}

public function testChildNonUpdatableColumnInsert(): int
{
$entity = new JoinedInheritanceNonUpdatableColumn();
$entity->nonUpdatableContent = 'foo';

$this->_em->persist($entity);
$this->_em->flush();

// check INSERT query doesn't change insertable entity property
self::assertEquals('foo', $entity->nonUpdatableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonUpdatableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonUpdatableColumn::class, $entity);
self::assertEquals('foo', $entity->nonUpdatableContent);

return $entity->id;
}

/** @depends testChildNonUpdatableColumnInsert */
public function testChildNonUpdatableColumnUpdate(int $entityId): void
{
$entity = $this->_em->find(JoinedInheritanceNonUpdatableColumn::class, $entityId);
self::assertInstanceOf(JoinedInheritanceNonUpdatableColumn::class, $entity);
self::assertEquals('foo', $entity->nonUpdatableContent);

// update exist entity
$entity->nonUpdatableContent = 'bar';
// change some property to ensure UPDATE query will be done
self::assertNotEquals('bar', $entity->rootField);
$entity->rootField = 'bar';

$this->_em->persist($entity);
$this->_em->flush();

// check UPDATE query cause set database value into non-updatable entity property
self::assertEquals('foo', $entity->nonUpdatableContent);

// check other process get same state
$this->_em->clear();
$entity = $this->_em->find(JoinedInheritanceNonUpdatableColumn::class, $entity->id);
self::assertInstanceOf(JoinedInheritanceNonUpdatableColumn::class, $entity);
self::assertEquals('bar', $entity->rootField); // check that UPDATE query done
self::assertEquals('foo', $entity->nonUpdatableContent);
}
}
Loading

0 comments on commit b5987ad

Please sign in to comment.