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.
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
.
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.
Shouldn't the line:
Actually be:
And BTW, excellent write up!