Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cascade deletion complicate-referenced entities does not work [ForeignKeyConstraintViolationException] #5665

Open
mtal opened this issue Feb 14, 2016 · 19 comments
Assignees
Labels

Comments

@mtal
Copy link

mtal commented Feb 14, 2016

[Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException]             
  An exception occurred while executing 'DELETE FROM subitem WHERE id = ?' wi  
  th params [4]:                                                               
  SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or upda  
  te a parent row: a foreign key constraint fails (`shop`.`item`, CONSTRAINT   
  `FK_1F1B251EB3AE4213` FOREIGN KEY (`featured_item_id`) REFERENCES `subitem`  
   (`id`))                                                                     

is caused by

        $item = new Item();
        $sub1 = new Subitem();
        $sub2 = new Subitem();
        $item->addItem($sub1);
        $item->addItem($sub2);
        $item->setFeaturedItem($sub2);
        $em->persist($item);
        $em->flush();
        $em->remove($item);
        $em->flush();

Entity mappings:

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Item
{

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="Subitem", mappedBy="item", cascade={"all"}, orphanRemoval=true)
     */
    protected $items;

    /**
     * @var Subitem
     *
     * @ORM\ManyToOne(targetEntity="Subitem")
     */
    protected $featuredItem;

    public function __construct()
    {
        $this->items = new ArrayCollection();
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return ArrayCollection
     */
    public function getItems()
    {
        return $this->items;
    }

    public function addItem(Subitem $item)
    {
        $this->items[] = $item;
        $item->setItem($this);
    }

    public function removeItem(Subitem $item  = null) {
        if ($this->featuredItem === $item) {
            $this->featuredItem = null;
        }
        $this->items->removeElement($item);
    }

    /**
     * @return Subitem
     */
    public function getFeaturedItem()
    {
        return $this->featuredItem;
    }

    /**
     * @param Subitem $featuredItem
     */
    public function setFeaturedItem(Subitem $featuredItem)
    {
        $this->featuredItem = $featuredItem;
    }

}

/**
 * @ORM\Entity
 */
class Subitem
{

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var Item
     *
     * @ORM\ManyToOne(targetEntity="Item", inversedBy="items")
     */
    protected $item;

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return Item
     */
    public function getItem()
    {
        return $this->item;
    }

    /**
     * @param Item $item
     */
    public function setItem($item)
    {
        $this->item = $item;
    }

}
@Ocramius
Copy link
Member

Seems normal to me:

$em->remove($item); item now marked for removal

$em->flush(); tries to delete the item, but it is still referenced by sub-items, which recursively are referenced by Item#featuredItem

In this scenario, you need to define a DB-level cascade operation via the @JoinColumn(onDelete="CASCADE") mapping.

@Ocramius Ocramius self-assigned this Feb 14, 2016
@mtal
Copy link
Author

mtal commented Feb 14, 2016

DB-level cascade will not trigger lifecycle events.

If this scenario seem normal to you, I could give you another
Before marking item for deletion with $em->remove($item); manually delete subitems by $em->remove($sub1) and $em->remove($sub2) or by orphan ($item->removeItem($sub1); $item->removeItem($sub2);)

The flush is still throw ForeignKeyConstraintViolationException.

Entities cannot be removed at ORM level without calling flush more than 1 time. Is this normal?

@Ocramius
Copy link
Member

DB-level cascade will not trigger lifecycle events.

Indeed, but a delete with a cascade won't trigger any deletes until mid-flush, so lifecycle events on some cascaded items will still work (first level of cascading)

@Ocramius
Copy link
Member

Entities cannot be removed at ORM level without calling flush more than 1 time. Is this normal?

You have a cyclic constraint there: that's not something the ORM can solve for you right now

@mtal
Copy link
Author

mtal commented Feb 14, 2016

You have a cyclic constraint there: that's not something the ORM can solve for you right now

But it solve insertions properly by preassigning NULL in INSERT query and assigning generated identy in update query.

So it should do the same at deletion in reverse order. Update column to NULL before deleting.

@Ocramius
Copy link
Member

Update column to NULL before deleting.

That isn't currently done by the ORM as far as I know (I would have to check the test suite, but no time ATM), mostly because the column may be non-nullable

@mtal
Copy link
Author

mtal commented Feb 14, 2016

That isn't currently done by the ORM as far as I know (I would have to check the test suite, but no time ATM), mostly because the column may be non-nullable

In this case it is impossible. If it is non-nullable you cannot insert cyclic referenced entity. So first flush for persisting will fail.

@Ocramius
Copy link
Member

Good point indeed. I suggest digging in the existing test suite for similar cases though, as this behavior may indeed be improved.

@guilhermeblanco
Copy link
Member

@mtal I'd ask you to try the same case using latest master.
I refactored the CommitOrderCalculator which should address the cyclic insertion. There's one use case though that was not covered (as it was way too hard to address and test), and I wanna confirm this is the same case you just found out.

I also implemented a couple changes in the OrphanRemoval that should properly remove orphaned on *ToMany and might be related.

Can you please test this against master and comment back? We might extract a sample TestCase from this for us to look if both situations didn't cover your issue.

@mtal
Copy link
Author

mtal commented Feb 16, 2016

Delete order is changed but issue is still persist. Exception is still thrown.

 [Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException]             
  An exception occurred while executing 'DELETE FROM item WHERE id = ?' with   
  params [2]:                                                                  
  SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or upda  
  te a parent row: a foreign key constraint fails (`shop`.`subitem`, CONSTRAI  
  NT `FK_C6AE5795126F525E` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`))

The commit order cannot fix it. Before deleting row, another that references it, must be updated. Referencing column must be updated to null.

I refactored the CommitOrderCalculator which should address the cyclic insertion.

The cyclic insertion was work properly before. The issue in impossibility to delete inside single flush.

@mtal
Copy link
Author

mtal commented Feb 16, 2016

And without actual (persisted cyclic references) deletion still fails. Commit order calculated incorrectly.

Look at this case:

        $em->beginTransaction();
        $item = new Item();
        $sub = new Subitem();
        $item->addItem($sub);
        $subsub = new Subsubitem();
        $sub->setSub($subsub);
        $em->persist($item);
        $em->flush();
        $em->remove($item);
        $em->flush();
        $em->rollback();

Exception
[Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException] An exception occurred while executing 'DELETE FROM item WHERE id = ?' with params [3]: SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or upda te a parent row: a foreign key constraint fails (shop.subitem, CONSTRAI NTFK_C6AE5795126F525EFOREIGN KEY (item_id) REFERENCESitem(id))

The database log

           19 Query START TRANSACTION
           19 Query INSERT INTO subsubitem (id) VALUES (null)
           19 Query INSERT INTO item (featured_item_id) VALUES (NULL)
           19 Query INSERT INTO subitem (item_id, sub_id) VALUES (3, 1)
           19 Query DELETE FROM item WHERE id = 3

The 'subitem' and 'subsubitem' must be deleted before item.

Mapping:

<?php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Item
{

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="Subitem", mappedBy="item", cascade={"all"}, orphanRemoval=true)
     */
    protected $items;

    /**
     * @var Subitem
     *
     * @ORM\ManyToOne(targetEntity="Subitem")
     */
    protected $featuredItem;

    public function __construct()
    {
        $this->items = new ArrayCollection();
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return ArrayCollection
     */
    public function getItems()
    {
        return $this->items;
    }

    public function addItem(Subitem $item)
    {
        $this->items[] = $item;
        $item->setItem($this);
    }

    public function removeItem(Subitem $item) {
        if ($this->featuredItem === $item) {
            $this->featuredItem = null;
        }
        $this->items->removeElement($item);
    }

    /**
     * @return Subitem
     */
    public function getFeaturedItem()
    {
        return $this->featuredItem;
    }

    /**
     * @param Subitem $featuredItem
     */
    public function setFeaturedItem(Subitem $featuredItem = null)
    {
        $this->featuredItem = $featuredItem;
    }

}

/**
 * @ORM\Entity
 */
class Subitem
{

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var Item
     *
     * @ORM\ManyToOne(targetEntity="Item", inversedBy="items")
     */
    protected $item;

    /**
     * @var Subsubitem
     *
     * @ORM\OneToOne(targetEntity="Subsubitem", cascade={"all"})
     */
    protected $sub;

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return Item
     */
    public function getItem()
    {
        return $this->item;
    }

    /**
     * @param Item $item
     */
    public function setItem($item)
    {
        $this->item = $item;
    }

    /**
     * @return Subsubitem
     */
    public function getSub()
    {
        return $this->sub;
    }

    /**
     * @param Subsubitem $sub
     */
    public function setSub(Subsubitem $sub = null)
    {
        $this->sub = $sub;
    }

}

/**
 * @ORM\Entity
 */
class Subsubitem
{

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

}

@idchlife
Copy link

idchlife commented Sep 19, 2016

Guys, any info on resolving this issue? I have user->galleries->photos->images and I have foreign key constraint error mentioned in topic when trying to remove user.

I should make onDelete="CASCADE" in this case or remove everything by hand?

UPDATE: onDelete="CASCADE" does not event make difference for migration to database, so I cannot use this.

@slince
Copy link

slince commented Dec 12, 2017

up

@Ocramius
Copy link
Member

@slince that doesn't help - open a PR with a reproducible failing test case

@slince
Copy link

slince commented Dec 12, 2017

@Ocramius

Hi, the problem I encountered is the same as this one

@Ocramius
Copy link
Member

@slince yes, but upvoting doesn't get anywhere on a project without dedicated staff. You gotta help yourself too ;-)

@wilariz
Copy link

wilariz commented Oct 15, 2018

Hi, are there a solution for this problem? recently I update mi libraries and a update process trigger this error.
Checking the class UnitOfWork, I have the list of entities to delete ( var $collectionDeletions), in mi case I have 3 entities(a parent and two children), the problem that I found is in the order to delete, it try to delete the parent first, and children after, it acoording to the order of the list.

my mapping aparently is ok, "persist" and "remove" from owner and onDelete:Cascade from other side.

Any idea about solution?

mpdude added a commit to mpdude/doctrine2 that referenced this issue Feb 28, 2023
…ut DB-level cascade

This adds a failing test case for doctrine#5665.

In this example, we have a cyclic association between three entities. All associations are NULLable, so the ORM is able to perform the INSERT operation: The cycle can be broken by scheduling an "extra UPDATE" in the UoW.

However, the UoW is unable to perform the remove operation. Cyclic references by the foreign keys in the database prevent removal of two of the entities.

If the ORM were able to detect this case and perform an UPDATE _before_ the DELETE, the test would pass.

As a workaround, `@JoinColumn(onDelete="CASCADE")` can be used. That way, the DBMS will make this UPDATE just in time and without ORM support – but that seems not to be what the OP of doctrine#5665 asked for.
@mpdude
Copy link
Contributor

mpdude commented Feb 28, 2023

Update 2023:

To summarize what has been written above, the problem is not finding an insert order for the cyclic associations. The UoW is able to solve this by scheduling "extra updates". This is possible since all associations are NULLable (by default!).

The problem lies in the DELETE operation, where the ORM does currently not schedule extra updates before the DELETE to break association cycles.

A failing test showing the lack of this feature can be found in #10548.

But even when the user configures @JoinColumn(onDelete="CASCADE")] to make the DBMS null out the foreign keys, it is important that the commit order takes this into consideration and schedules deletions appropriately. It is not currently doing this, which is demonstrated in #10566.

mpdude added a commit to mpdude/doctrine2 that referenced this issue May 8, 2023
When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

#### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place. 

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

#### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`. 

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve doctrine#10531 or doctrine#10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

#### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support: 
* doctrine#5665
* doctrine#10548
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 8, 2023
When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

#### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place. 

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

#### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`. 

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve doctrine#10531 or doctrine#10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

#### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support: 
* doctrine#5665
* doctrine#10548
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 8, 2023
When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

#### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place. 

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

#### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`. 

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve doctrine#10531 or doctrine#10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

#### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support: 
* doctrine#5665
* doctrine#10548
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 8, 2023
When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

#### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place. 

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

#### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`. 

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve doctrine#10531 or doctrine#10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

#### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support: 
* doctrine#5665
* doctrine#10548
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 23, 2023
When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

 #### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place.

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

 #### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`.

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve doctrine#10531 or doctrine#10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

 #### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support:
* doctrine#5665
* doctrine#10548
SenseException pushed a commit that referenced this issue May 30, 2023
…10566)

When computing the commit order for entity removals, we have to look out for `@ORM\JoinColumn(onDelete="SET NULL")` to find places where cyclic associations can be broken.

 #### Background

The UoW computes a "commit order" to find the sequence in which tables shall be processed when inserting entities into the database or performing delete operations.

For the insert case, the ORM is able to schedule _extra updates_ that will be performed after all entities have been inserted. Associations which are configured as `@ORM\JoinColumn(nullable=true, ...)` can be left as `NULL` in the database when performing the initial `INSERT` statements, and will be updated once all new entities have been written to the database. This can be used to break cyclic associations between entity instances.

For removals, the ORM does not currently implement up-front `UPDATE` statements to `NULL` out associations before `DELETE` statements are executed. That means when associations form a cycle, users have to configure `@ORM\JoinColumn(onDelete="SET NULL", ...)` on one of the associations involved. This transfers responsibility to the DBMS to break the cycle at that place.

_But_, we still have to perform the delete statements in an order that makes this happen early enough. This may be a _different_ order than the one required for the insert case. We can find it _only_ by looking at the `onDelete` behaviour. We must ignore the `nullable` property, which is irrelevant, since we do not even try to `NULL` anything.

 #### Example

Assume three entity classes `A`, `B`, `C`. There are unidirectional one-to-one associations `A -> B`, `B -> C`, `C -> A`. All those associations are `nullable= true`.

Three entities `$a`, `$b`, `$c` are created from these respective classes and associations are set up.

All operations `cascade` at the ORM level. So we can test what happens when we start the operations at the three individual entities, but in the end, they will always involve all three of them.

_Any_ insert order will work, so the improvements necessary to solve #10531 or #10532 are not needed here. Since all associations are between different tables, the current table-level computation is good enough.

For the removal case, only the `A -> B` association has `onDelete="SET NULL"`. So, the only possible execution order is `$b`, `$c`, `$a`. We have to find that regardless of where we start the cascade operation.

The DBMS will set the `A -> B` association on `$a` to `NULL` when we remove `$b`. We can then remove `$c` since it is no longer being referred to, then `$a`.

 #### Related cases

These cases ask for the ORM to perform the extra update before the delete by itself, without DBMS-level support:
* #5665
* #10548
mpdude added a commit to mpdude/doctrine2 that referenced this issue Oct 8, 2024
…ut DB-level cascade

This adds a failing test case for doctrine#5665.

In this example, we have a cyclic association between three entities. All associations are NULLable, so the ORM is able to perform the INSERT operation: The cycle can be broken by scheduling an "extra UPDATE" in the UoW.

However, the UoW is unable to perform the remove operation. Cyclic references by the foreign keys in the database prevent removal of two of the entities.

If the ORM were able to detect this case and perform an UPDATE _before_ the DELETE, the test would pass.

As a workaround, `@JoinColumn(onDelete="CASCADE")` can be used. That way, the DBMS will make this UPDATE just in time and without ORM support – but that seems not to be what the OP of doctrine#5665 asked for.
@mpdude
Copy link
Contributor

mpdude commented Oct 8, 2024

Update 2024:

The ORM is not (yet?) able to perform extra UPDATEs to NULL out associations referring to entities that are about to be removed; but, a database-level ON DELETE SET NULL can be used for that.

Since #10566, the ORM should schedule removals in the appropriate order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants