Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/2.16.x' into 3.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
greg0ire committed Jun 24, 2023
2 parents 6fa95b9 + 70bcff7 commit 2285713
Show file tree
Hide file tree
Showing 22 changed files with 744 additions and 226 deletions.
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
"require-dev": {
"doctrine/coding-standard": "^12.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "1.10.14",
"phpstan/phpstan": "1.10.18",
"phpunit/phpunit": "^10.0.14",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^5.4 || ^6.0",
"symfony/cache": "^5.4 || ^6.2",
"symfony/var-exporter": "^5.4 || ^6.2",
"vimeo/psalm": "5.11.0"
"vimeo/psalm": "5.12.0"
},
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",
Expand Down
18 changes: 18 additions & 0 deletions docs/en/reference/association-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,15 @@ Generated MySQL Schema:
replaced by one-to-many/many-to-one associations between the 3
participating classes.

.. note::

For many-to-many associations, the ORM takes care of managing rows
in the join table connecting both sides. Due to the way it deals
with entity removals, database-level constraints may not work the
way one might intuitively assume. Thus, be sure not to miss the section
on :ref:`join table management <remove_object_many_to_many_join_tables>`.


Many-To-Many, Bidirectional
---------------------------

Expand Down Expand Up @@ -646,6 +655,15 @@ one is bidirectional.
The MySQL schema is exactly the same as for the Many-To-Many
uni-directional case above.

.. note::

For many-to-many associations, the ORM takes care of managing rows
in the join table connecting both sides. Due to the way it deals
with entity removals, database-level constraints may not work the
way one might intuitively assume. Thus, be sure not to miss the section
on :ref:`join table management <remove_object_many_to_many_join_tables>`.


Owning and Inverse Side on a ManyToMany Association
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
34 changes: 34 additions & 0 deletions docs/en/reference/native-sql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,40 @@ The first parameter is the name of the column in the SQL result set
and the second parameter is the result alias under which the value
of the column will be placed in the transformed Doctrine result.

Special case: DTOs
...................

You can also use ``ResultSetMapping`` to map the results of a native SQL
query to a DTO (Data Transfer Object). This is done by adding scalar
results for each argument of the DTO's constructor, then filling the
``newObjectMappings`` property of the ``ResultSetMapping`` with
information about where to map each scalar result:

.. code-block:: php
<?php
$rsm = new ResultSetMapping();
$rsm->addScalarResult('name', 1, 'string');
$rsm->addScalarResult('email', 2, 'string');
$rsm->addScalarResult('city', 3, 'string');
$rsm->newObjectMappings['name'] = [
'className' => CmsUserDTO::class,
'objIndex' => 0, // a result can contain many DTOs, this is the index of the DTO to map to
'argIndex' => 0, // each scalar result can be mapped to a different argument of the DTO constructor
];
$rsm->newObjectMappings['email'] = [
'className' => CmsUserDTO::class,
'objIndex' => 0,
'argIndex' => 1,
];
$rsm->newObjectMappings['city'] = [
'className' => CmsUserDTO::class,
'objIndex' => 0,
'argIndex' => 2,
];
Meta results
~~~~~~~~~~~~

Expand Down
50 changes: 43 additions & 7 deletions docs/en/reference/working-with-objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,17 +286,53 @@ as follows:
After an entity has been removed, its in-memory state is the same as
before the removal, except for generated identifiers.

Removing an entity will also automatically delete any existing
records in many-to-many join tables that link this entity. The
action taken depends on the value of the ``@joinColumn`` mapping
attribute "onDelete". Either Doctrine issues a dedicated ``DELETE``
statement for records of each join table or it depends on the
foreign key semantics of onDelete="CASCADE".
During the ``EntityManager#flush()`` operation, the removed entity
will also be removed from all collections in entities currently
loaded into memory.

.. _remove_object_many_to_many_join_tables:

Join-table management when removing from many-to-many collections
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Regarding existing rows in many-to-many join tables that refer to
an entity being removed, the following applies.

When the entity being removed does not declare the many-to-many association
itself (that is, the many-to-many association is unidirectional and
the entity is on the inverse side), the ORM has no reasonable way to
detect associations targeting the entity's class. Thus, no ORM-level handling
of join-table rows is attempted and database-level constraints apply.
In case of database-level ``ON DELETE RESTRICT`` constraints, the
``EntityManager#flush()`` operation may abort and a ``ConstraintViolationException``
may be thrown. No in-memory collections will be modified in this case.
With ``ON DELETE CASCADE``, the RDBMS will take care of removing rows
from join tables.

When the entity being removed is part of bi-directional many-to-many
association, either as the owning or inverse side, the ORM will
delete rows from join tables before removing the entity itself. That means
database-level ``ON DELETE RESTRICT`` constraints on join tables are not
effective, since the join table rows are removed first. Removal of join table
rows happens through specialized methods in entity and collection persister
classes and take one query per entity and join table. In case the association
uses a ``@JoinColumn`` configuration with ``onDelete="CASCADE"``, instead
of using a dedicated ``DELETE`` query the database-level operation will be
relied upon.

.. note::

In case you rely on database-level ``ON DELETE RESTRICT`` constraints,
be aware that by making many-to-many associations bidirectional the
assumed protection may be lost.


Performance of different deletion strategies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Deleting an object with all its associated objects can be achieved
in multiple ways with very different performance impacts.


1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
will fetch this association. If its a Single association it will
pass this entity to
Expand Down
1 change: 0 additions & 1 deletion lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@ static function (string $fieldName) use ($data, $class) {
* that belongs to a particular component/class. Afterwards, all these chunks
* are processed, one after the other. For each chunk of class data only one of the
* following code paths is executed:
*
* Path A: The data chunk belongs to a joined/associated object and the association
* is collection-valued.
* Path B: The data chunk belongs to a joined/associated object and the association
Expand Down
1 change: 0 additions & 1 deletion lib/Doctrine/ORM/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use BackedEnum;
use BadMethodCallException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
Expand Down
1 change: 1 addition & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ private function completeIdGeneratorMapping(ClassMetadata $class): void
case ClassMetadata::GENERATOR_TYPE_IDENTITY:
$sequenceName = null;
$fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null;
$platform = $this->getTargetPlatform();

$generator = $fieldName && $class->fieldMappings[$fieldName]->type === 'bigint'
? new BigIntegerIdentityGenerator()
Expand Down
11 changes: 10 additions & 1 deletion lib/Doctrine/ORM/Persisters/Collection/OneToManyPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use function array_values;
use function assert;
use function implode;
use function is_int;
use function is_string;

/**
Expand Down Expand Up @@ -154,16 +155,22 @@ private function deleteEntityCollection(PersistentCollection $collection): int
$targetClass = $this->em->getClassMetadata($mapping->targetEntity);
$columns = [];
$parameters = [];
$types = [];

foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) {
$columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
$parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)];
$types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em);
}

$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';

return $this->conn->executeStatement($statement, $parameters);
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);

assert(is_int($numAffected));

return $numAffected;
}

/**
Expand Down Expand Up @@ -228,6 +235,8 @@ private function deleteJoinedEntityCollection(PersistentCollection $collection):

$this->conn->executeStatement($statement);

assert(is_int($numDeleted));

return $numDeleted;
}

Expand Down
10 changes: 1 addition & 9 deletions lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,6 @@ public function executeInserts(): array
$paramIndex = 1;

foreach ($insertData[$tableName] as $column => $value) {
if ($value instanceof BackedEnum) {
$value = $value->value;
}

$stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
}
}
Expand Down Expand Up @@ -503,7 +499,7 @@ final protected function updateTable(
protected function deleteJoinTableRecords(array $identifier, array $types): void
{
foreach ($this->class->associationMappings as $mapping) {
if (! $mapping->isManyToMany()) {
if (! $mapping->isManyToMany() || $mapping->isOnDeleteCascade) {
continue;
}

Expand Down Expand Up @@ -539,10 +535,6 @@ protected function deleteJoinTableRecords(array $identifier, array $types): void
$otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
}

if (isset($mapping->isOnDeleteCascade)) {
continue;
}

$joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);

$this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);
Expand Down
13 changes: 12 additions & 1 deletion lib/Doctrine/ORM/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;
use Throwable;

use function array_flip;
use function str_replace;
Expand Down Expand Up @@ -192,7 +193,17 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister

$identifier = $classMetadata->getIdentifierValues($proxy);

if ($entityPersister->loadById($identifier, $proxy) === null) {
try {
$entity = $entityPersister->loadById($identifier, $proxy);
} catch (Throwable $exception) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);

throw $exception;
}

if ($entity === null) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
Expand Down
8 changes: 4 additions & 4 deletions lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\ORM\Query\Exec;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
Expand Down Expand Up @@ -49,10 +50,9 @@ public function removeQueryCacheProfile(): void
/**
* Executes all sql statements.
*
* @param Connection $conn The database connection that is used to execute the queries.
* @psalm-param list<mixed>|array<string, mixed> $params The parameters.
* @psalm-param array<int, int|string|Type|null>|
* array<string, int|string|Type|null> $types The parameter types.
* @param Connection $conn The database connection that is used to execute the queries.
* @param list<mixed>|array<string, mixed> $params The parameters.
* @param array<int<0, max>, string|Type|ArrayParameterType::*>|array<string, string|Type|ArrayParameterType::*> $types The parameter types.
*/
abstract public function execute(Connection $conn, array $params, array $types): Result|int;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ protected function configure(): void
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setFilterSchemaAssetsExpression($regexp);
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ protected function configure(): void
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setFilterSchemaAssetsExpression($regexp);
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ protected function configure(): void
by the ORM, you can use a DBAL functionality to filter the tables and sequences down
on a global level:
$config->setFilterSchemaAssetsExpression($regexp);
$config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool {
if ($assetName instanceof AbstractAsset) {
$assetName = $assetName->getName();
}
return !str_starts_with($assetName, 'audit_');
});
EOT);
}

Expand Down
Loading

0 comments on commit 2285713

Please sign in to comment.