Skip to content

Instantly share code, notes, and snippets.

@pylebecq
Last active December 16, 2024 14:15
Show Gist options
  • Save pylebecq/f844d1f6860241d8b025 to your computer and use it in GitHub Desktop.
Save pylebecq/f844d1f6860241d8b025 to your computer and use it in GitHub Desktop.

What's the difference between cascade="remove" and orphanRemoval=true in Doctrine 2

TLDR: The cascade={"remove"} is like a "software" onDelete="CASCADE", and will remove objects from the database only when an explicit call to $em->remove() occurs. Thus, it could result in more than one object being deleted. orphanRemoval can remove objects from the database even if there was no explicit call to ->remove().

I answered this question a few times to different people so I will try to sum things up in this Gist.

Let's take two entities A and B as an example. I will use a OneToOne relationship in this example but it works exactly the same with OneToMany relationships.

class A
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="B", mappedBy="a")
     */
    protected $b;
    
    // [...]
}

class B
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="A", inversedBy="b")
     * @ORM\JoinColumn(name="a_id", referencedColumnName="id")
     */
    protected $a;
    
    // [...]
}

And then let's initialise some instances of these entities to be able to use them in the example:

// First, we create a correct association between instances of theses entities
$a = new A();
$b = new B();
$a->setB($b);
$b->setA($a);
$em->persist($a);
$em->persist($b);
$em->flush();

At this point, we have this in our database:

MariaDB [playground]> select * from a; select * from b;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

+----+------+
| id | a_id |
+----+------+
|  1 |    1 |
+----+------+
1 row in set (0.00 sec)

Ok, let's now try different operations on our data.

Understanding cascade={"remove"}

First, if we wanted to remove $a, we could do the following:

$em->remove($a);
$em->flush();

This does not work. This result in the following exception:

[Doctrine\DBAL\DBALException]
  An exception occurred while executing 'DELETE FROM A WHERE id = ?' with params [1]:
  SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a pare
  nt row: a foreign key constraint fails (`playground`.`b`, CONSTRAINT `FK_4AD0CF313BD
  E5358` FOREIGN KEY (`a_id`) REFERENCES `A` (`id`))

This is because by default, the database will prevent deletions of objects A which are referenced by B objects. To make this work, we could instruct to remove $b:

$em->remove($a);
$em->remove($b);
$em->flush(); // Works

However, if we have many relationships with other objects not represented in our example, this could be painful. So, if we wanted to remove all B objects referencing A objects without explicitely removing them, we could change A to look like this:

class A
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="B", mappedBy="a", cascade={"remove"})
     */
    protected $b;
    
    // [...]
}

This asks Doctrine to cascade the remove instruction to objects B referencing the A object we want to remove. So our first example, which was not working, is now working thanks to the cascade={"remove"} on the relationship with B.

$em->remove($a);
$em->flush(); // Works

The cascade={"remove"} option is well suited when we want our objects to fulfill the following sentence: "When we remove a A object, we want all B objects referencing this A object to also be removed". It works exactly as we would expect a onDelete=CASCADE to work when specified in our database, except that it takes place in your PHP application instead of in the database. We could achieve the exact same result by removing the cascade={"persist"} and putting a onDelete="CASCADE" in B:

class A
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="B", mappedBy="a")
     */
    protected $b;
    
    // [...]
}

class B
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="A", inversedBy="b")
     * @ORM\JoinColumn(name="a_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $a;
    
    // [...]
}
$em->remove($a);
$em->flush(); // Works

They achieve the same result but differently:

  • cascade={"remove"} will issue two queries to the database: the first one will instruct to remove $b and the second will instruct to remove $a.
  • onDelete="CASCADE" will issue only one query which will instruct to remove $a and the database knows it must also remove $b objects referencing $a.

Understanding orphanRemoval=true

Let's get back to our original scenario:

class A
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="B", mappedBy="a")
     */
    protected $b;
    
    // [...]
}

class B
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="A", inversedBy="b")
     * @ORM\JoinColumn(name="a_id", referencedColumnName="id")
     */
    protected $a;
    
    // [...]
}

Now, we don't want to remove A, but we want to remove the relationship between $a and $b:

$a->setB(null);
$em->flush(); // No error, but does nothing

The data is the database are still the same as in the beginning:

MariaDB [playground]> select * from a; select * from b;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

+----+------+
| id | a_id |
+----+------+
|  1 |    1 |
+----+------+
1 row in set (0.00 sec)

This might be a surprise but the code above does nothing because we are modifying the inverse side of the relationship. And modifying only the inverse side of the relationship will not do anything. Owning and inverse sides are not in the scope of this article so if you need to know more, please read carefully the documentation.

This is were the orphanRemoval option can be used. It will instruct Doctrine to look at the inverse side of the relationship to determine if the B object is still referenced by the A object. And if it is not the case, then the B object is considered orphan, and having this option to true means we don't want orphans. Thus, Doctrine will remove it from the database:

class A
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="B", mappedBy="a", orphanRemoval=true)
     */
    protected $b;
    
    // [...]
}

class B
{
    // [...]
    
    /**
     * @ORM\OneToOne(targetEntity="A", inversedBy="b")
     * @ORM\JoinColumn(name="a_id", referencedColumnName="id")
     */
    protected $a;
    
    // [...]
}
$a->setB(null);
$em->flush(); // Works, B is removed
MariaDB [playground]> select * from a; select * from b;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

Empty set (0.00 sec)

We could achieve the same result simply by explicitely removing $b.

The orphanRemoval option is particularely useful when not used with OneToOne relationships but rather OneToMany ones, because all we need to do is to add/remove objects from the collection and Doctrine will figure out if the objects need to be persisted (you will need the cascade={"persist"} for that if there are some new entities added) or removed.

One funny thing to note is that orphanRemoval sometimes achieve the same result as cascade={"remove"}, because when using it and removing $a, Doctrine is smart enough to know the $b object will result in an orphan and so it will issue a query to remove it. You end up with both A and B object being removed, as when using the cascade.

@s001dxp
Copy link

s001dxp commented Jun 7, 2018

Shouldn't the line:

We could achieve the exact same result by removing the cascade={"persist"} and putting a onDelete="CASCADE" in B:

Actually be:

We could achieve the exact same result by removing the cascade={"remove"} and putting a onDelete="CASCADE" in B:

And BTW, excellent write up!

@chadyred
Copy link

Got it !

@alaindet
Copy link

Insert "Thank you!" GIF from The Office here

@Radiergummi
Copy link

@alaindet got you covered. Thanks from me too :)

michael-scott-the-office

@maximsymfony
Copy link

Thank you!! Excellent !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment