Versioning doesn't work with a custom type primary key #5118

Closed
opened 2026-01-22 14:58:50 +01:00 by admin · 3 comments
Owner

Originally created by @ngrevet on GitHub (Apr 29, 2016).

I've been wondering for a while why does Doctrine resets my @Version entity field to 0 every time I flush to the database even though the correct version can be found in the database, so I decided to dig down into the entrails of Doctrine and found some peculiar issue.

When the execution flow reaches the Doctrine\ORM\Persisters\Entity\BasicEntityPersister::fetchVersionValue method (it's the point after the flush where Doctrine tries to retrieve the new version of the entity from the database to re-hydrate the entity) apparently it assumes that the primary key for this table is a known primitive type and doesn't bother trying to map it to anything. More specifically this line:

// Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:346
$value = $this->conn->fetchColumn($sql, array_values($flatId));

The third and fourth parameters of fetchColumn should be 0 and the type(s) of the $flatId field(s). But nothing is sent. In my situation, my primary key is a custom type uuid_binary. Then down the line when you get to Doctrine\DBAL\Connection::executeQuery and the statement is being prepared, since there is no types involved in the $types parameter, the binding actually never happens!

// Doctrine/DBAL/Driver/PDOStatement.php:825
$stmt = $this->_conn->prepare($query);
if ($types) {
    $this->_bindTypedValues($stmt, $params, $types);
    $stmt->execute();
} else {
    $stmt->execute($params);
}

Which means that the versioning query...

SELECT version FROM tablename WHERE primarykey = ?

... doesn't ever receive any value for the primarykey placeholder and the request ultimately fails and returns false. Doctrine then takes this false and assigns it to the @Version field of the entity in Doctrine\ORM\Persisters\Entity\BasicEntityPersister::assignDefaultVersionValue, which then translates to 0.

// Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:317
protected function assignDefaultVersionValue($entity, array $id)
{
    $value = $this->fetchVersionValue($this->class, $id);
    $this->class->setFieldValue($entity, $this->class->versionField, $value);
}

So it seems like this scenario is entirely missing.

Originally created by @ngrevet on GitHub (Apr 29, 2016). I've been wondering for a while why does Doctrine resets my `@Version` entity field to `0` every time I flush to the database even though the correct version can be found in the database, so I decided to dig down into the entrails of Doctrine and found some peculiar issue. When the execution flow reaches the `Doctrine\ORM\Persisters\Entity\BasicEntityPersister::fetchVersionValue` method (it's the point after the flush where Doctrine tries to retrieve the new version of the entity from the database to re-hydrate the entity) apparently it assumes that the primary key for this table is a known primitive type and doesn't bother trying to map it to anything. More specifically this line: ``` php // Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:346 $value = $this->conn->fetchColumn($sql, array_values($flatId)); ``` The third and fourth parameters of `fetchColumn` should be `0` and the type(s) of the `$flatId` field(s). But nothing is sent. In my situation, my primary key is a custom type `uuid_binary`. Then down the line when you get to `Doctrine\DBAL\Connection::executeQuery` and the statement is being prepared, since there is no types involved in the `$types` parameter, the binding actually never happens! ``` php // Doctrine/DBAL/Driver/PDOStatement.php:825 $stmt = $this->_conn->prepare($query); if ($types) { $this->_bindTypedValues($stmt, $params, $types); $stmt->execute(); } else { $stmt->execute($params); } ``` Which means that the versioning query... ``` sql SELECT version FROM tablename WHERE primarykey = ? ``` ... doesn't ever receive any value for the `primarykey` placeholder and the request ultimately fails and returns `false`. Doctrine then takes this `false` and assigns it to the `@Version` field of the entity in `Doctrine\ORM\Persisters\Entity\BasicEntityPersister::assignDefaultVersionValue`, which then translates to `0`. ``` php // Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:317 protected function assignDefaultVersionValue($entity, array $id) { $value = $this->fetchVersionValue($this->class, $id); $this->class->setFieldValue($entity, $this->class->versionField, $value); } ``` So it seems like this scenario is entirely missing.
admin added the Bug label 2026-01-22 14:58:50 +01:00
admin closed this issue 2026-01-22 14:58:51 +01:00
Author
Owner

@ngrevet commented on GitHub (Apr 29, 2016):

This fix works:

    protected function fetchVersionValue($versionedClass, array $id)
    {
        $versionField    = $versionedClass->versionField;
        $tableName       = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
        $identifier      = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
        $columnName      = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform);

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

+        $types = [];
+        foreach ($identifier as $fieldName) {
+            $types[] = $versionedClass->fieldMappings[$fieldName]['type'];
+        }
        $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);

-        $value = $this->conn->fetchColumn($sql, array_values($flatId));
+        $value = $this->conn->fetchColumn($sql, array_values($flatId), 0, $types);

        return Type::getType($versionedClass->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->platform);
    }
@ngrevet commented on GitHub (Apr 29, 2016): This fix works: ``` diff protected function fetchVersionValue($versionedClass, array $id) { $versionField = $versionedClass->versionField; $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); $columnName = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform); // FIXME: Order with composite keys might not be correct $sql = 'SELECT ' . $columnName . ' FROM ' . $tableName . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; + $types = []; + foreach ($identifier as $fieldName) { + $types[] = $versionedClass->fieldMappings[$fieldName]['type']; + } $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); - $value = $this->conn->fetchColumn($sql, array_values($flatId)); + $value = $this->conn->fetchColumn($sql, array_values($flatId), 0, $types); return Type::getType($versionedClass->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->platform); } ```
Author
Owner

@alle commented on GitHub (Jun 3, 2017):

Yes, mate, gg! Why don't you make a PR?

@alle commented on GitHub (Jun 3, 2017): Yes, mate, gg! Why don't you make a PR?
Author
Owner

@ngrevet commented on GitHub (Jun 5, 2017):

Thanks, I'm not well versed in PR on open source projects, I figured someone would probably end up using that work eventually.

@ngrevet commented on GitHub (Jun 5, 2017): Thanks, I'm not well versed in PR on open source projects, I figured someone would probably end up using that work eventually.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#5118