Skip to content

Commit

Permalink
Generated/Virtual Columns: Insertable / Updateable
Browse files Browse the repository at this point in the history
Refactoring reg. change request
- Fetch not insertable / updateable columns after an insert / update and set them on the entity.
doctrine#5728
  • Loading branch information
mehldau committed Nov 24, 2021
1 parent f33505e commit a154ced
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 33 deletions.
21 changes: 19 additions & 2 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,14 @@ class ClassMetadataInfo implements ClassMetadata
*/
public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;

/**
* READ-ONLY: A Flag indicating whether or not one or more columns of this class have to be reloaded
* after insert / update operations.
*
* @var bool
*/
public $requiresFetchAfterUpdate = false;

/**
* READ-ONLY: A flag for whether or not instances of this class are to be versioned
* with optimistic locking.
Expand Down Expand Up @@ -2688,6 +2696,10 @@ public function mapField(array $mapping)
$mapping = $this->validateAndCompleteFieldMapping($mapping);
$this->assertFieldNotMapped($mapping['fieldName']);

if (isset($mapping['notInsertable']) || isset($mapping['notUpdateable'])) {
$this->requiresFetchAfterUpdate = true;
}

$this->fieldMappings[$mapping['fieldName']] = $mapping;
}

Expand Down Expand Up @@ -3418,8 +3430,9 @@ public function setSequenceGeneratorDefinition(array $definition)
*/
public function setVersionMapping(array &$mapping)
{
$this->isVersioned = true;
$this->versionField = $mapping['fieldName'];
$this->isVersioned = true;
$this->versionField = $mapping['fieldName'];
$this->requiresFetchAfterUpdate = true;

if (! isset($mapping['default'])) {
if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
Expand All @@ -3442,6 +3455,10 @@ public function setVersionMapping(array &$mapping)
public function setVersioned($bool)
{
$this->isVersioned = $bool;

if ($bool) {
$this->requiresFetchAfterUpdate = true;
}
}

/**
Expand Down
59 changes: 39 additions & 20 deletions lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\ORM\Utility\PersisterHelper;
use LengthException;

use function array_combine;
use function array_map;
use function array_merge;
use function array_search;
use function array_unique;
use function array_values;
use function array_keys;
use function assert;
use function count;
use function get_class;
Expand Down Expand Up @@ -287,8 +289,8 @@ public function executeInserts()
$id = $this->class->getIdentifierValues($entity);
}

if ($this->class->isVersioned) {
$this->assignDefaultVersionValue($entity, $id);
if ($this->class->requiresFetchAfterUpdate) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}

Expand All @@ -300,50 +302,67 @@ public function executeInserts()
/**
* Retrieves the default version value which was created
* by the preceding INSERT statement and assigns it back in to the
* entities version field.
* entities version field if the given entity is versioned.
* Also retrieves values of columns marked as 'non insertable' and / or
* 'not updatetable' and assigns them back to the entities corresponding fields.
*
* @param object $entity
* @param mixed[] $id
*
* @return void
*/
protected function assignDefaultVersionValue($entity, array $id)
protected function assignDefaultVersionAndUpsertableValues($entity, array $id)
{
$value = $this->fetchVersionValue($this->class, $id);
$values = $this->fetchVersionAndNotUpsertableValues($this->class, $id);

$this->class->setFieldValue($entity, $this->class->versionField, $value);
foreach ($values as $field => $value) {
$value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform);

$this->class->setFieldValue($entity, $field, $value);
}
}

/**
* Fetches the current version value of a versioned entity.
* Fetches the current version value of a versioned entity and / or the values of fields
* marked as 'not insertable' and / or 'not updateable'.
*
* @param ClassMetadata $versionedClass
* @param mixed[] $id
*
* @return mixed
*/
protected function fetchVersionValue($versionedClass, array $id)
protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
{
$versionField = $versionedClass->versionField;
$fieldMapping = $versionedClass->fieldMappings[$versionField];
$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
$columnName = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform);
$columnNames = [];
foreach ($this->class->fieldMappings as $key => $column) {
if (isset($column['notInsertable']) || isset($column['notUpdateable']) || ($this->class->isVersioned && $key === $versionedClass->versionField)) {
$columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform);
}
}

$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);

// FIXME: Order with composite keys might not be correct
$sql = 'SELECT ' . $columnName
. ' FROM ' . $tableName
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
$sql = 'SELECT ' . implode(', ', $columnNames)
. ' FROM ' . $tableName
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';

$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);

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

return Type::getType($fieldMapping['type'])->convertToPHPValue($value, $this->platform);
$values = array_combine(array_keys($columnNames), array_values($values));

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

return $values;
}

/**
Expand Down Expand Up @@ -386,10 +405,10 @@ public function update($entity)

$this->updateTable($entity, $quotedTableName, $data, $isVersioned);

if ($isVersioned) {
if ($this->class->requiresFetchAfterUpdate) {
$id = $this->class->getIdentifierValues($entity);

$this->assignDefaultVersionValue($entity, $id);
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}
}

Expand Down
26 changes: 16 additions & 10 deletions lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Internal\SQLResultCasing;
use Doctrine\ORM\Mapping\ClassMetadata;
Expand Down Expand Up @@ -168,8 +169,8 @@ public function executeInserts()
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
}

if ($this->class->isVersioned) {
$this->assignDefaultVersionValue($entity, $id);
if ($this->class->requiresFetchAfterUpdate) {
$this->assignDefaultVersionAndUpsertableValues($entity, $id);
}

// Execute inserts on subtables.
Expand Down Expand Up @@ -225,18 +226,18 @@ public function update($entity)
$this->updateTable($entity, $tableName, $data, $versioned);
}

// Make sure the table with the version column is updated even if no columns on that
// table were affected.
if ($isVersioned) {
if (! isset($updateData[$versionedTable])) {
if ($this->class->requiresFetchAfterUpdate) {
// Make sure the table with the version column is updated even if no columns on that
// table were affected.
if ($isVersioned && ! isset($updateData[$versionedTable])) {
$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);

$this->updateTable($entity, $tableName, [], true);
}

$identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity);

$this->assignDefaultVersionValue($entity, $identifiers);
$this->assignDefaultVersionAndUpsertableValues($entity, $identifiers);
}
}

Expand Down Expand Up @@ -549,10 +550,15 @@ protected function getInsertColumnList()
/**
* {@inheritdoc}
*/
protected function assignDefaultVersionValue($entity, array $id)
protected function assignDefaultVersionAndUpsertableValues($entity, array $id)
{
$value = $this->fetchVersionValue($this->getVersionedClassMetadata(), $id);
$this->class->setFieldValue($entity, $this->class->versionField, $value);
$values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id);

foreach ($values as $field => $value) {
$value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform);

$this->class->setFieldValue($entity, $field, $value);
}
}

private function getJoinSql(string $baseTableAlias): string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
use Doctrine\ORM\Persisters\Exception\NonUpdateableField;
use Doctrine\Tests\Mocks\ArrayResultFactory;
use Doctrine\Tests\Models\Upsertable\Insertable;
use Doctrine\Tests\Models\Upsertable\Updateable;
use Doctrine\Tests\OrmTestCase;
Expand Down Expand Up @@ -36,7 +37,7 @@ public function testInsertSQLUsesInsertableColumns(): void
self::assertEquals('INSERT INTO insertable_column (insertableContent, insertableContentDefault) VALUES (?, ?)', $method->invoke($persister));
}

public function testUpdateSQLUsesUpdateableColumns(): void
public function testUpdateSQLUsesUpdateableColumnsAndNonUpdateableColumnIsReloadedDueToDatabaseModification(): void
{
$persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(Updateable::class));

Expand All @@ -52,11 +53,14 @@ public function testUpdateSQLUsesUpdateableColumns(): void
$this->entityManager->getUnitOfWork()->propertyChanged($entity, 'updateableContent', 'default', 'persistable');
$this->entityManager->getUnitOfWork()->propertyChanged($entity, 'updateableContentDefault', 'default', 'persistable');

$this->entityManager->getConnection()->setQueryResult(ArrayResultFactory::createFromArray([['modifed-by-trigger']]));

$persister->update($entity);

$executeStatements = $this->entityManager->getConnection()->getExecuteStatements();

self::assertEquals('UPDATE updateable_column SET updateableContent = ?, updateableContentDefault = ? WHERE id = ?', $executeStatements[0]['sql']);
self::assertSame($entity->nonUpdateableContent, 'modifed-by-trigger');
}

public function testExceptionIsThrownWhenTryingToChangeNonUpdateableColumn(): void
Expand Down

0 comments on commit a154ced

Please sign in to comment.