mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
831a1eb7d2 | ||
|
|
3a82b153f3 | ||
|
|
168ac31084 | ||
|
|
fe4a2e83cf | ||
|
|
205b2f5f20 | ||
|
|
3f550c19e3 | ||
|
|
8ac6a13ca0 | ||
|
|
2707b09a07 | ||
|
|
121158f92c | ||
|
|
51ad860a25 | ||
|
|
9bd51aaeb6 | ||
|
|
1fe1a6a048 | ||
|
|
c37b115450 | ||
|
|
19129e9f8a | ||
|
|
c1bb2ccf4b | ||
|
|
e3d7c6076c |
@@ -232,6 +232,33 @@ vendors SQL parser to show us further errors in the parsing
|
||||
process, for example if the Unit would not be one of the supported
|
||||
values by MySql.
|
||||
|
||||
Typed functions
|
||||
---------------
|
||||
By default, result of custom functions is fetched as-is from the database driver.
|
||||
If you want to be sure that the type is always the same, then your custom function needs to
|
||||
implement ``Doctrine\ORM\Query\AST\TypedExpression``. Then, the result is wired
|
||||
through ``Doctrine\DBAL\Types\Type::convertToPhpValue()`` of the ``Type`` returned in ``getReturnType()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\TypedExpression;
|
||||
|
||||
class DateDiff extends FunctionNode implements TypedExpression
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getReturnType(): Type
|
||||
{
|
||||
return Type::getType(Types::INTEGER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
|
||||
@@ -338,10 +338,11 @@ 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
|
||||
``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``.
|
||||
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
|
||||
fetch this association. If it's a Single association it will pass
|
||||
this entity to ``EntityManager#remove()``. If the association is a
|
||||
collection, Doctrine will loop over all its elements and pass them to
|
||||
``EntityManager#remove()``.
|
||||
In both cases the cascade remove semantics are applied recursively.
|
||||
For large object graphs this removal strategy can be very costly.
|
||||
2. Using a DQL ``DELETE`` statement allows you to delete multiple
|
||||
|
||||
@@ -139,12 +139,12 @@ step:
|
||||
|
||||
// Create a simple "default" Doctrine ORM configuration for Attributes
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration(
|
||||
paths: array(__DIR__."/src"),
|
||||
paths: [__DIR__ . '/src'],
|
||||
isDevMode: true,
|
||||
);
|
||||
// or if you prefer XML
|
||||
// $config = ORMSetup::createXMLMetadataConfiguration(
|
||||
// paths: array(__DIR__."/config/xml"),
|
||||
// paths: [__DIR__ . '/config/xml'],
|
||||
// isDevMode: true,
|
||||
//);
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ class AttributeDriver implements MappingDriver
|
||||
$metadata->mapOneToMany($mapping);
|
||||
} elseif ($manyToOneAttribute !== null) {
|
||||
if ($metadata->isEmbeddedClass) {
|
||||
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
|
||||
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToOne::class);
|
||||
}
|
||||
|
||||
$idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class);
|
||||
|
||||
@@ -40,7 +40,15 @@ class NativeQuery extends AbstractQuery
|
||||
$types = [];
|
||||
|
||||
foreach ($this->getParameters() as $parameter) {
|
||||
$name = $parameter->getName();
|
||||
$name = $parameter->getName();
|
||||
|
||||
if ($parameter->typeWasSpecified()) {
|
||||
$parameters[$name] = $parameter->getValue();
|
||||
$types[$name] = $parameter->getType();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->processParameterValue($parameter->getValue());
|
||||
$type = $parameter->getValue() === $value
|
||||
? $parameter->getType()
|
||||
|
||||
@@ -14,9 +14,12 @@ use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
|
||||
use function array_fill;
|
||||
use function array_keys;
|
||||
use function array_reverse;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
@@ -174,9 +177,12 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
|
||||
if ($targetClass->isInheritanceTypeSingleTable()) {
|
||||
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
|
||||
$statement .= ' AND ' . $discriminatorColumn->name . ' = ?';
|
||||
$parameters[] = $targetClass->discriminatorValue;
|
||||
$types[] = $discriminatorColumn->type;
|
||||
$discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap);
|
||||
$statement .= ' AND ' . $discriminatorColumn->name . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')';
|
||||
foreach ($discriminatorValues as $discriminatorValue) {
|
||||
$parameters[] = $discriminatorValue;
|
||||
$types[] = $discriminatorColumn->type;
|
||||
}
|
||||
}
|
||||
|
||||
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
|
||||
|
||||
@@ -792,17 +792,42 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
$computedIdentifier = [];
|
||||
|
||||
/** @var array<string,mixed>|null $sourceEntityData */
|
||||
$sourceEntityData = null;
|
||||
|
||||
// TRICKY: since the association is specular source and target are flipped
|
||||
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
|
||||
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
|
||||
throw MappingException::joinColumnMustPointToMappedField(
|
||||
$sourceClass->name,
|
||||
$sourceKeyColumn,
|
||||
);
|
||||
}
|
||||
// The likely case here is that the column is a join column
|
||||
// in an association mapping. However, there is no guarantee
|
||||
// at this point that a corresponding (generally identifying)
|
||||
// association has been mapped in the source entity. To handle
|
||||
// this case we directly reference the column-keyed data used
|
||||
// to initialize the source entity before throwing an exception.
|
||||
$resolvedSourceData = false;
|
||||
if (! isset($sourceEntityData)) {
|
||||
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
|
||||
}
|
||||
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
if (isset($sourceEntityData[$sourceKeyColumn])) {
|
||||
$dataValue = $sourceEntityData[$sourceKeyColumn];
|
||||
if ($dataValue !== null) {
|
||||
$resolvedSourceData = true;
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $resolvedSourceData) {
|
||||
throw MappingException::joinColumnMustPointToMappedField(
|
||||
$sourceClass->name,
|
||||
$sourceKeyColumn,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$targetEntity = $this->load($computedIdentifier, null, $assoc);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
|
||||
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse')]
|
||||
class InverseSide
|
||||
{
|
||||
/** Associative id (owning identifier) */
|
||||
#[Id]
|
||||
#[OneToOne(targetEntity: InverseSideIdTarget::class, inversedBy: 'inverseSide')]
|
||||
#[JoinColumn(nullable: false, name: 'associativeId')]
|
||||
public InverseSideIdTarget $associativeId;
|
||||
|
||||
#[OneToOne(targetEntity: OwningSide::class, mappedBy: 'inverse')]
|
||||
public OwningSide $owning;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse_id_target')]
|
||||
class InverseSideIdTarget
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'string', length: 255)]
|
||||
#[GeneratedValue(strategy: 'NONE')]
|
||||
public string $id;
|
||||
|
||||
#[OneToOne(targetEntity: InverseSide::class, mappedBy: 'associativeId')]
|
||||
public InverseSide $inverseSide;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_owning')]
|
||||
class OwningSide
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'string', length: 255)]
|
||||
#[GeneratedValue(strategy: 'NONE')]
|
||||
public string $id;
|
||||
|
||||
/** Owning side */
|
||||
#[OneToOne(targetEntity: InverseSide::class, inversedBy: 'owning')]
|
||||
#[JoinColumn(name: 'inverse', referencedColumnName: 'associativeId')]
|
||||
public InverseSide $inverse;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSide;
|
||||
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSideIdTarget;
|
||||
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\OwningSide;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function assert;
|
||||
|
||||
class OneToOneInverseSideWithAssociativeIdLoadAfterDqlQueryTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createSchemaForModels(OwningSide::class, InverseSideIdTarget::class, InverseSide::class);
|
||||
}
|
||||
|
||||
#[Group('GH-11108')]
|
||||
public function testInverseSideWithAssociativeIdOneToOneLoadedAfterDqlQuery(): void
|
||||
{
|
||||
$owner = new OwningSide();
|
||||
$inverseId = new InverseSideIdTarget();
|
||||
$inverse = new InverseSide();
|
||||
|
||||
$owner->id = 'owner';
|
||||
$inverseId->id = 'inverseId';
|
||||
$inverseId->inverseSide = $inverse;
|
||||
$inverse->associativeId = $inverseId;
|
||||
$owner->inverse = $inverse;
|
||||
$inverse->owning = $owner;
|
||||
|
||||
$this->_em->persist($owner);
|
||||
$this->_em->persist($inverseId);
|
||||
$this->_em->persist($inverse);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$fetchedInverse = $this
|
||||
->_em
|
||||
->createQueryBuilder()
|
||||
->select('inverse')
|
||||
->from(InverseSide::class, 'inverse')
|
||||
->andWhere('inverse.associativeId = :associativeId')
|
||||
->setParameter('associativeId', 'inverseId')
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
assert($fetchedInverse instanceof InverseSide);
|
||||
|
||||
self::assertInstanceOf(InverseSide::class, $fetchedInverse);
|
||||
self::assertInstanceOf(InverseSideIdTarget::class, $fetchedInverse->associativeId);
|
||||
self::assertInstanceOf(OwningSide::class, $fetchedInverse->owning);
|
||||
|
||||
$this->assertSQLEquals(
|
||||
'select o0_.associativeid as associativeid_0 from one_to_one_inverse_side_assoc_id_load_inverse o0_ where o0_.associativeid = ?',
|
||||
$this->getLastLoggedQuery(1)['sql'],
|
||||
);
|
||||
|
||||
$this->assertSQLEquals(
|
||||
'select t0.id as id_1, t0.inverse as inverse_2 from one_to_one_inverse_side_assoc_id_load_owning t0 where t0.inverse = ?',
|
||||
$this->getLastLoggedQuery()['sql'],
|
||||
);
|
||||
}
|
||||
}
|
||||
105
tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Normal file
105
tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11501Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11501AbstractTestEntity::class,
|
||||
GH11501TestEntityOne::class,
|
||||
GH11501TestEntityTwo::class,
|
||||
GH11501TestEntityHolder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @throws ORMException */
|
||||
public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void
|
||||
{
|
||||
$testEntityOne = new GH11501TestEntityOne();
|
||||
$testEntityTwo = new GH11501TestEntityTwo();
|
||||
$testEntityHolder = new GH11501TestEntityHolder();
|
||||
|
||||
$testEntityOne->testEntityHolder = $testEntityHolder;
|
||||
$testEntityHolder->testEntities->add($testEntityOne);
|
||||
|
||||
$testEntityTwo->testEntityHolder = $testEntityHolder;
|
||||
$testEntityHolder->testEntities->add($testEntityTwo);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($testEntityOne);
|
||||
$em->persist($testEntityTwo);
|
||||
$em->persist($testEntityHolder);
|
||||
$em->flush();
|
||||
|
||||
$testEntityHolder->testEntities = new ArrayCollection();
|
||||
$em->persist($testEntityHolder);
|
||||
$em->flush();
|
||||
$em->refresh($testEntityHolder);
|
||||
|
||||
static::assertEmpty($testEntityHolder->testEntities->toArray(), 'All records should have been deleted');
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'one_to_many_single_table_inheritance_test_entities_parent_join')]
|
||||
#[ORM\InheritanceType('SINGLE_TABLE')]
|
||||
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
|
||||
#[ORM\DiscriminatorMap([
|
||||
'test_entity_one' => 'GH11501TestEntityOne',
|
||||
'test_entity_two' => 'GH11501TestEntityTwo',
|
||||
])]
|
||||
class GH11501AbstractTestEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\GeneratedValue]
|
||||
public int $id;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: 'GH11501TestEntityHolder', inversedBy: 'testEntities')]
|
||||
#[ORM\JoinColumn(name: 'test_entity_holder_id', referencedColumnName: 'id')]
|
||||
public GH11501TestEntityHolder $testEntityHolder;
|
||||
}
|
||||
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11501TestEntityOne extends GH11501AbstractTestEntity
|
||||
{
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11501TestEntityTwo extends GH11501AbstractTestEntity
|
||||
{
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11501TestEntityHolder
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\GeneratedValue]
|
||||
public int $id;
|
||||
|
||||
#[ORM\OneToMany(
|
||||
targetEntity: 'GH11501AbstractTestEntity',
|
||||
mappedBy: 'testEntityHolder',
|
||||
orphanRemoval: true,
|
||||
)]
|
||||
public Collection $testEntities;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->testEntities = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
42
tests/Tests/ORM/Query/NativeQueryTest.php
Normal file
42
tests/Tests/ORM/Query/NativeQueryTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Query;
|
||||
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Tests\Mocks\EntityManagerMock;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
|
||||
class NativeQueryTest extends OrmTestCase
|
||||
{
|
||||
/** @var EntityManagerMock */
|
||||
protected $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManager = $this->getTestEntityManager();
|
||||
}
|
||||
|
||||
public function testValuesAreNotBeingResolvedForSpecifiedParameterTypes(): void
|
||||
{
|
||||
$unitOfWork = $this->createMock(UnitOfWork::class);
|
||||
|
||||
$this->entityManager->setUnitOfWork($unitOfWork);
|
||||
|
||||
$unitOfWork
|
||||
->expects(self::never())
|
||||
->method('getSingleIdentifierValue');
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
|
||||
$query = $this->entityManager->createNativeQuery('SELECT d.* FROM date_time_model d WHERE d.datetime = :value', $rsm);
|
||||
|
||||
$query->setParameter('value', new DateTime(), Types::DATETIME_MUTABLE);
|
||||
|
||||
self::assertEmpty($query->getResult());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user