18/02/2017 - DOCTRINE, SYMFONY
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.
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.
inversedBy
attribute of the ManyToOne mapping declaration.mappedBy
attribute of the OneToMany mapping declaration.nullable=false
forces a composition relationship so if you want to create a Team then you have to have a League to go with it.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)
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" [] []
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" [] []
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" [] []
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" [] []
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.