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

Support not Insertable/Updateable columns for entities with JOINED inheritance type #10598

Merged
merged 4 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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;
greg0ire marked this conversation as resolved.
Show resolved Hide resolved

/** @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