Skip to content
Merged
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
Add test: Commit order calculation must happen on the entity level
Add tests for entity insertion and deletion that require the commit order calculation to happen on the entity level. This demonstrates the necessity for the changes in #10547.

This PR contains two tests with carefully constructed entity relationships, where we have a non-nullable `parent` foreign key relationships between entities stored in the same table.

Class diagram:

```mermaid
classDiagram
    direction LR
    class A
    class B
    A --> B : parent
    B --|> A
```

Object graph:

```mermaid
graph LR;
    b1 --> b2;
    b2 --> a;
    b3 --> b2;
```

 #### Situation before #10547

The commit order is computed by looking at the associations at the _table_ (= _class_) level. Once the ordering of _tables_ has been found, entities being mapped to the same table will be processed in the order they were given to `persist()` or `remove()`.

That means only a particular ordering of `persist()` or `remove()` calls (see comment in the test) works:

For inserts, the order must be `$a, $b2, $b1, $b3` (or `... $b3, $b1`), for deletions `$b1, $b3, $b2, $a`.

 #### Situation with entity-level commit order computation (as in #10547)

The ORM computes the commit order by considering associations at the _entity_ level. It will be able to find a working order by itself.
  • Loading branch information
mpdude committed Jun 23, 2023
commit aa3ff458c761cae40a8d49c5ac4108a2bd5f2205
151 changes: 151 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH10531Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;

class GH10531Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(
GH10531A::class,
GH10531B::class
);
}

public function tearDown(): void
{
$conn = static::$sharedConn;
$conn->executeStatement('DELETE FROM gh10531_b');
$conn->executeStatement('DELETE FROM gh10531_a');
}

public function testInserts(): void
{
$a = new GH10531A();
$b1 = new GH10531B();
$b2 = new GH10531B();
$b3 = new GH10531B();

$b1->parent = $b2;
$b3->parent = $b2;
$b2->parent = $a;

/*
* The following would force a working commit order, but that's not what
* we want (the ORM shall sort this out internally).
*
* $this->_em->persist($a);
* $this->_em->persist($b2);
* $this->_em->flush();
* $this->_em->persist($b1);
* $this->_em->persist($b3);
* $this->_em->flush();
*/

// Pass $b2 to persist() between $b1 and $b3, so that any potential reliance upon the
// order of persist() calls is spotted: No matter if it is in the order that persist()
// was called or the other way round, in both cases there is an entity that will come
// "before" $b2 but depend on its primary key, so the ORM must re-order the inserts.

$this->_em->persist($a);
$this->_em->persist($b1);
$this->_em->persist($b2);
$this->_em->persist($b3);
$this->_em->flush();

self::assertNotNull($a->id);
self::assertNotNull($b1->id);
self::assertNotNull($b2->id);
self::assertNotNull($b3->id);
}

public function testDeletes(): void
{
$this->expectNotToPerformAssertions();
$con = $this->_em->getConnection();

// The "a" entity
$con->insert('gh10531_a', ['id' => 1, 'discr' => 'A']);
$a = $this->_em->find(GH10531A::class, 1);

// The "b2" entity
$con->insert('gh10531_a', ['id' => 2, 'discr' => 'B']);
$con->insert('gh10531_b', ['id' => 2, 'parent_id' => 1]);
$b2 = $this->_em->find(GH10531B::class, 2);

// The "b1" entity
$con->insert('gh10531_a', ['id' => 3, 'discr' => 'B']);
$con->insert('gh10531_b', ['id' => 3, 'parent_id' => 2]);
$b1 = $this->_em->find(GH10531B::class, 3);

// The "b3" entity
$con->insert('gh10531_a', ['id' => 4, 'discr' => 'B']);
$con->insert('gh10531_b', ['id' => 4, 'parent_id' => 2]);
$b3 = $this->_em->find(GH10531B::class, 4);

/*
* The following would make the deletions happen in an order
* where the not-nullable foreign key constraints would not be
* violated. But, we want the ORM to be able to sort this out
* internally.
*
* $this->_em->remove($b1);
* $this->_em->remove($b3);
* $this->_em->remove($b2);
*/

// As before, put $b2 in between $b1 and $b3 so that the order of the
// remove() calls alone (in either direction) does not solve the problem.
// The ORM will have to sort $b2 to be deleted last, after $b1 and $b3.
$this->_em->remove($b1);
$this->_em->remove($b2);
$this->_em->remove($b3);

$this->_em->flush();
}
}

/**
* @ORM\Entity
* @ORM\Table(name="gh10531_a")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({ "A": "GH10531A", "B": "GH10531B" })
* @ORM\InheritanceType("JOINED")
*
* We are using JTI here, since STI would relax the not-nullable constraint for the "parent"
* column (it has to be NULL when the row contains a GH10531A instance). Causes another error,
* but not the constraint violation I'd like to point out.
*/
class GH10531A
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
}

/**
* @ORM\Entity
* @ORM\Table(name="gh10531_b")
*/
class GH10531B extends GH10531A
{
/**
* @ORM\ManyToOne(targetEntity="GH10531A")
* @ORM\JoinColumn(nullable=false, name="parent_id")
*
* @var GH10531A
*/
public $parent;
}