Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/Cache/Persister/Entity/AbstractEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@ public function refresh(array $id, object $entity, LockMode|int|null $lockMode =
$this->persister->refresh($id, $entity, $lockMode);
}

public function usesReturningClause(): bool
{
return $this->persister->usesReturningClause();
}

/** @param array<string, mixed> $ownerId */
protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId, /* string $filterHash */): CollectionCacheKey
{
Expand Down
10 changes: 10 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -723,4 +723,14 @@ public function getEagerFetchBatchSize(): int
{
return $this->attributes['fetchModeSubselectBatchSize'] ?? 100;
}

public function setUseReturningClauseForGeneratingId(bool $flag): void
{
$this->attributes['useReturningClauseForGeneratingId'] = $flag;
}

public function getUseReturningClauseForGeneratingId(): bool
{
return $this->attributes['useReturningClauseForGeneratingId'] ?? false;
}
}
68 changes: 57 additions & 11 deletions src/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDb1052Platform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
Expand Down Expand Up @@ -164,6 +167,8 @@ class BasicEntityPersister implements EntityPersister

private string|null $filterHash = null;

private bool $shouldUseReturningClause;

/**
* Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
* and persists instances of the class described by the given ClassMetadata descriptor.
Expand All @@ -174,20 +179,23 @@ public function __construct(
protected EntityManagerInterface $em,
protected ClassMetadata $class,
) {
$this->conn = $em->getConnection();
$this->platform = $this->conn->getDatabasePlatform();
$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
$this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
$this->conn = $em->getConnection();
$this->platform = $this->conn->getDatabasePlatform();
$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
$this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
$class,
new Query\ResultSetMapping(),
false,
);
$this->limitsHandlingContext = new CachedPersisterContext(
$this->limitsHandlingContext = new CachedPersisterContext(
$class,
new Query\ResultSetMapping(),
true,
);
$this->shouldUseReturningClause = $class->isIdGeneratorIdentity()
&& $em->getConfiguration()->getUseReturningClauseForGeneratingId()
&& $this->supportsReturningClause($this->platform);
}

final protected function isFilterHashUpToDate(): bool
Expand Down Expand Up @@ -247,11 +255,16 @@ public function executeInserts(): void
}
}

$stmt->executeStatement();
$result = $stmt->executeQuery();

if ($isPostInsertId) {
$generatedId = $idGenerator->generateId($this->em, $entity);
$id = [$this->class->identifier[0] => $generatedId];
if ($this->shouldUseReturningClause) {
$generatedId = $result->fetchOne();
} else {
$generatedId = $idGenerator->generateId($this->em, $entity);
}

$id = [$this->class->identifier[0] => $generatedId];

$uow->assignPostInsertId($entity, $generatedId);
} else {
Expand Down Expand Up @@ -1413,7 +1426,16 @@ public function getInsertSQL(): string
if ($columns === []) {
$identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);

return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
$insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);

if ($this->shouldUseReturningClause) {
$insertSql .= sprintf(
' RETURNING %s',
$this->quoteStrategy->getColumnName($this->class->getSingleIdentifierFieldName(), $this->class, $this->platform),
);
}

return $insertSql;
}

$placeholders = [];
Expand All @@ -1437,7 +1459,26 @@ public function getInsertSQL(): string
$columns = implode(', ', $columns);
$placeholders = implode(', ', $placeholders);

return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
$insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);

if ($this->shouldUseReturningClause) {
$insertSql .= sprintf(
' RETURNING %s',
$this->quoteStrategy->getColumnName($this->class->getSingleIdentifierFieldName(), $this->class, $this->platform),
);
}

return $insertSql;
}

/**
* This should be replaced with a method call from AbstractPlatform
*/
private function supportsReturningClause(AbstractPlatform $platform): bool
{
return $platform instanceof PostgreSQLPlatform
|| $platform instanceof MariaDb1052Platform // lowest version 10.5.0 (https://mariadb.com/kb/en/insertreturning/)
|| $platform instanceof SqlitePlatform; // lowest version 3.35.0 (https://www.sqlite.org/lang_returning.html)
Comment on lines +1474 to +1481
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR in dbal should be opened for this if this gets accepted

}

/**
Expand Down Expand Up @@ -2003,4 +2044,9 @@ static function ($fieldName) use ($class, $entityManager): string {
$class->identifier,
);
}

public function usesReturningClause(): bool
{
return $this->shouldUseReturningClause;
}
}
2 changes: 2 additions & 0 deletions src/Persisters/Entity/EntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,6 @@ public function getOneToManyCollection(
* Checks whether the given managed entity exists in the database.
*/
public function exists(object $entity, Criteria|null $extraConditions = null): bool;

public function usesReturningClause(): bool;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i had to add this to interface because JoinedSubclassPersister::executeInserts invokes root persister

}
11 changes: 8 additions & 3 deletions src/Persisters/Entity/JoinedSubclassPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,16 @@ public function executeInserts(): void
$rootTableStmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]);
}

$rootTableStmt->executeStatement();
$result = $rootTableStmt->executeQuery();

if ($isPostInsertId) {
$generatedId = $idGenerator->generateId($this->em, $entity);
$id = [$this->class->identifier[0] => $generatedId];
if ($rootPersister->usesReturningClause()) {
$generatedId = $result->fetchOne();
} else {
$generatedId = $idGenerator->generateId($this->em, $entity);
}

$id = [$this->class->identifier[0] => $generatedId];

$uow->assignPostInsertId($entity, $generatedId);
} else {
Expand Down
22 changes: 22 additions & 0 deletions tests/Tests/Models/Identity/Cat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\Identity;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;

#[Entity]
class Cat
{
#[Id]
#[Column(type: 'integer')]
#[GeneratedValue(strategy: 'IDENTITY')]
public int $id;

#[Column(type: 'string')]
public string $name;
}
19 changes: 19 additions & 0 deletions tests/Tests/Models/Identity/Dog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\Identity;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;

#[Entity]
class Dog
{
#[Id]
#[Column(type: 'integer')]
#[GeneratedValue(strategy: 'IDENTITY')]
public int $id;
}
27 changes: 27 additions & 0 deletions tests/Tests/ORM/Functional/StandardEntityPersisterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

namespace Doctrine\Tests\ORM\Functional;

use Doctrine\DBAL\Platforms\MariaDb1052Platform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Tests\Models\ECommerce\ECommerceCart;
use Doctrine\Tests\Models\ECommerce\ECommerceCategory;
use Doctrine\Tests\Models\ECommerce\ECommerceCustomer;
use Doctrine\Tests\Models\ECommerce\ECommerceFeature;
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
Expand Down Expand Up @@ -107,4 +111,27 @@ public function testAddPersistRetrieve(): void
// Persisted Product now must have 3 Feature items
self::assertCount(3, $res[0]->getFeatures());
}

public function testInsertWithReturningClause(): void
{
$this->_em->getConfiguration()->setUseReturningClauseForGeneratingId(true);

$c = new ECommerceCategory();
$c->setName('Electronics');

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

$platform = $this->_em->getConnection()->getDatabasePlatform();
if (
// this should be replaced with a method call from AbstractPlatform
$platform instanceof PostgreSQLPlatform
|| $platform instanceof MariaDb1052Platform
|| $platform instanceof SqlitePlatform
) {
self::assertStringEndsWith('RETURNING id', $this->getLastLoggedQuery()['sql']);
} else {
self::assertStringEndsNotWith('RETURNING id', $this->getLastLoggedQuery()['sql']);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Persisters;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
use Doctrine\Tests\Mocks\EntityManagerMock;
use Doctrine\Tests\Models\Identity\Cat;
use Doctrine\Tests\Models\Identity\Dog;
use Doctrine\Tests\OrmTestCase;
use ReflectionMethod;

class BasicEntityPersisterReturningClauseTest extends OrmTestCase
{
private EntityManagerMock $entityManager;

protected function setUp(): void
{
parent::setUp();

$evm = $this->createMock(EventManager::class);
$evm->method('hasListeners')
->willReturn(false);

$platform = $this->createMock(PostgreSQLPlatform::class);
$platform->method('getEmptyIdentityInsertSQL')
->willReturnCallback(static function (string $table, string $column): string {
return 'INSERT INTO ' . $table . ' (' . $column . ') VALUES (DEFAULT)';
});

$connection = $this->createMock(Connection::class);
$connection->method('getDatabasePlatform')
->willReturn($platform);
$connection->method('getEventManager')
->willReturn($evm);

$this->entityManager = $this->createTestEntityManagerWithConnection($connection);
$this->entityManager->getConfiguration()->setUseReturningClauseForGeneratingId(true);
}

public function testEmptyIdentityInsertSQL(): void
{
$persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(Dog::class));
$method = new ReflectionMethod($persister, 'getInsertSQL');
$method->setAccessible(true);

$sql = $method->invoke($persister);

self::assertSame('INSERT INTO Dog (id) VALUES (DEFAULT) RETURNING id', $sql);
}

public function testInsertSqlWithColumn(): void
{
$persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(Cat::class));
$method = new ReflectionMethod($persister, 'getInsertSQL');
$method->setAccessible(true);

$sql = $method->invoke($persister);

self::assertSame('INSERT INTO Cat (name) VALUES (?) RETURNING id', $sql);
}
}