In this post we're going to see how cascade={"remove"} and orphanRemoval=true work in bidirectional one-to-one relationships. The reason why we're using bidirectional relationship is because it provides navigational access in both directions. Our example is football focused.


Info


If you try to delete a record in an entity where you have cascade={"remove"} or orphanRemoval=true property set, it will try to delete records in other associated entities as well. Always be very careful when using cascade={"remove"} and orphanRemoval=true operations because you might end up (not knowingly) delete other records in other entities based on your ORM.


Design




class Country
{
/**
* @ORM\OneToOne(targetEntity="League", mappedBy="country")
*/
private $league;
}

class League
{
/**
* @ORM\OneToOne(targetEntity="Country", inversedBy="league")
* @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false)
*/
private $country;
}

`country` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
)

`league` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`country_id` int(11) NOT NULL,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_3EB4C318F92F3E70` (`country_id`),
CONSTRAINT `FK_3EB4C318F92F3E70` FOREIGN KEY (`country_id`) REFERENCES `country` (`id`)
)

mysql> SELECT
-> `country`.`id` AS CountryID,
-> `country`.`name` AS CountryName,
-> `league`.`id` AS LeagueID,
-> `league`.`name` AS LeagueName
-> FROM `country`
-> INNER JOIN `league` ON `country`.`id` = `league`.`country_id`;
+-----------+-------------+----------+----------------+
| CountryID | CountryName | LeagueID | LeagueName |
+-----------+-------------+----------+----------------+
| 1 | England | 1 | Premier League |
+-----------+-------------+----------+----------------+
1 rows in set (0.00 sec)

Tests


Cascade 1) No cascade at all


class Country
{
/**
* @ORM\OneToOne(targetEntity="League", mappedBy="country")
*/
private $league;
}

class League
{
/**
* @ORM\OneToOne(targetEntity="Country", inversedBy="league")
* @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false)
*/
private $country;
}

Deleting Country: Results in "Integrity constraint violation" error as League is Country dependent.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM country ... LEFT JOIN league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM country WHERE id = ? [1] []
doctrine.DEBUG: "ROLLBACK" [] []

Deleting League: League gets deleted but Country remains intact.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Cascade 2) Cascade on Country


If you used orphanRemoval=true instead, the result would be exactly the same.


class Country
{
/**
* @ORM\OneToOne(targetEntity="League", mappedBy="country", cascade={"remove"})
*/
private $league;
}

class League
{
/**
* @ORM\OneToOne(targetEntity="Country", inversedBy="league")
* @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false)
*/
private $country;
}

Deleting Country: Country gets deleted as well as League.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM country ... LEFT JOIN league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM country WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Deleting League: League gets deleted but Country remains intact.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Cascade 3) Cascade on League


If you used orphanRemoval=true instead, the result would be exactly the same.


class Country
{
/**
* @ORM\OneToOne(targetEntity="League", mappedBy="country")
*/
private $league;
}

class League
{
/**
* @ORM\OneToOne(targetEntity="Country", inversedBy="league", cascade={"remove"})
* @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false)
*/
private $country;
}

Deleting Country: Results in "Integrity constraint violation" error as League is Country dependent.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM country ... LEFT JOIN league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM country WHERE id = ? [1] []
doctrine.DEBUG: "ROLLBACK" [] []

Deleting League: League gets deleted as well as Country.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM country WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Cascade 4) Cascade on Country and League


If you used orphanRemoval=true instead, the result would be exactly the same.


class Country
{
/**
* @ORM\OneToOne(targetEntity="League", mappedBy="country", cascade={"remove"})
*/
private $league;
}

class League
{
/**
* @ORM\OneToOne(targetEntity="Country", inversedBy="league", cascade={"remove"})
* @ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false)
*/
private $country;
}

Deleting Country: Country gets deleted as well as League.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM country ... LEFT JOIN league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM country WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Deleting League: League gets deleted as well as Country.


$this->entityManager->remove($entity);
$this->entityManager->flush();

doctrine.DEBUG: SELECT ... FROM league ...
doctrine.DEBUG: SELECT ... FROM country ... LEFT JOIN league ...
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM country WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Result


In this example, Country is independent so whatever happens to League, we should NOT automatically delete Country behind the scene. At same time, League is Country dependent so if Country gets deleted then we should automatically delete League behind the scene. Based on this note, the best option for us to go with is, Cascade 2 where we put cascade={"remove"} or orphanRemoval=true only on Country.


Readings