In this post we're going to see how cascade={"remove"} and orphanRemoval=true work in bidirectional one-to-many 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 League
{
/**
* @ORM\OneToMany(targetEntity="Team", mappedBy="league")
*/
private $teams;

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

class Team
{
/**
* @ORM\ManyToOne(targetEntity="League", inversedBy="teams")
* @ORM\JoinColumn(name="league_id", referencedColumnName="id", nullable=false)
*/
private $league;
}

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

`team` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`league_id` int(11) NOT NULL,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
KEY `IDX_C4E0A61F58AFC4DE` (`league_id`),
CONSTRAINT `FK_C4E0A61F58AFC4DE` FOREIGN KEY (`league_id`) REFERENCES `league` (`id`)
)

mysql> SELECT
-> `league`.`id` AS LeagueID,
-> `league`.`name` AS LeagueName,
-> `team`.`id` AS TeamID,
-> `team`.`name` AS TeamName
-> FROM `league`
-> INNER JOIN `team` ON `league`.`id` = `team`.`league_id`
-> WHERE
-> `team`.`league_id` = 1;
+----------+------------+--------+-------------+
| LeagueID | LeagueName | TeamID | TeamName |
+----------+------------+--------+-------------+
| 1 | Super Lig | 1 | Fenerbahce |
| 1 | Super Lig | 2 | Galatasaray |
| 1 | Super Lig | 3 | Besiktas |
+----------+------------+--------+-------------+
3 rows in set (0.00 sec)

Tests


Cascade 1) No cascade at all


class League
{
/**
* @ORM\OneToMany(targetEntity="Team", mappedBy="league")
*/
private $teams;

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

class Team
{
/**
* @ORM\ManyToOne(targetEntity="League", inversedBy="teams")
* @ORM\JoinColumn(name="league_id", referencedColumnName="id", nullable=false)
*/
private $league;
}

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


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

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

Deleting Team: Team gets deleted but League remains intact.


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

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

Cascade 2) Cascade on League


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


class League
{
/**
* @ORM\OneToMany(targetEntity="Team", mappedBy="league", cascade={"remove"})
*/
private $teams;

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

class Team
{
/**
* @ORM\ManyToOne(targetEntity="League", inversedBy="teams")
* @ORM\JoinColumn(name="league_id", referencedColumnName="id", nullable=false)
*/
private $league;
}

Deleting League: League gets deleted as well as Team.


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

doctrine.DEBUG: SELECT ... FROM league WHERE id = ? LIMIT 1 ["1"] []
doctrine.DEBUG: SELECT ... FROM team WHERE league_id = ? [1] []
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [2] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [3] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Deleting Team: Team gets deleted but League remains intact.


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

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

Cascade 3) Cascade on Team


If you used orphanRemoval=true instead, you would get [Creation Error] The annotation @ORM\ManyToOne declared on property Team::$league does not have a property named "orphanRemoval". Available properties: targetEntity, cascade, fetch, inversedBy error.


class League
{
/**
* @ORM\OneToMany(targetEntity="Team", mappedBy="league")
*/
private $teams;

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

class Team
{
/**
* @ORM\ManyToOne(targetEntity="League", inversedBy="teams", cascade={"remove"})
* @ORM\JoinColumn(name="league_id", referencedColumnName="id", nullable=false)
*/
private $league;
}

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


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

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

Deleting Team: Results in "Integrity constraint violation" error as all Team records are League dependent.


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

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

Cascade 4) Cascade on League and Team


If you used orphanRemoval=true instead, you would get [Creation Error] The annotation @ORM\ManyToOne declared on property Team::$league does not have a property named "orphanRemoval". Available properties: targetEntity, cascade, fetch, inversedBy error.


class League
{
/**
* @ORM\OneToMany(targetEntity="Team", mappedBy="league", cascade={"remove"})
*/
private $teams;

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

class Team
{
/**
* @ORM\ManyToOne(targetEntity="League", inversedBy="teams", cascade={"remove"})
* @ORM\JoinColumn(name="league_id", referencedColumnName="id", nullable=false)
*/
private $league;
}

Deleting League: League gets deleted as well as Team.


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

doctrine.DEBUG: SELECT ... FROM league WHERE id = ? LIMIT 1 ["1"] []
doctrine.DEBUG: SELECT ... FROM team WHERE league_id = ? [1] []
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [2] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [3] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Deleting Team: Team gets deleted as well as League.


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

doctrine.DEBUG: SELECT ... FROM team WHERE id = ? LIMIT 1 ["1"] []
doctrine.DEBUG: SELECT ... FROM league WHERE id = ? [1] []
doctrine.DEBUG: SELECT ... FROM team WHERE league_id = ? [1] []
doctrine.DEBUG: "START TRANSACTION" [] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [2] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [3] []
doctrine.DEBUG: DELETE FROM team WHERE id = ? [1] []
doctrine.DEBUG: DELETE FROM league WHERE id = ? [1] []
doctrine.DEBUG: "COMMIT" [] []

Result


In this example, League is independent so whatever happens to Team, we should NOT automatically delete League behind the scene. At same time, Team is League dependent so if League gets deleted then we should automatically delete Team 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 League.


Readings