I'm trying to make a simple example in order to learn how to delete a row from a parent table and automatically delete the matching rows in the child table using Doctrine2.
Here are the two entities I'm using:
Child.php:
<?php
namespace Acme\CascadeBundle\Entity;
use Doctrine\开发者_运维百科ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="child")
*/
class Child {
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="Father", cascade={"remove"})
*
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="father_id", referencedColumnName="id")
* })
*
* @var father
*/
private $father;
}
Father.php
<?php
namespace Acme\CascadeBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="father")
*/
class Father
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
}
The tables are correctly created on the database, but the On Delete Cascade option it's not created. What am I doing wrong?
There are two kinds of cascades in Doctrine:
ORM level - uses
cascade={"remove"}
in the association - this is a calculation that is done in theUnitOfWork
and does not affect the database structure. When you remove an object, theUnitOfWork
will iterate over all objects in the association and remove them.Database level - uses
onDelete="CASCADE"
on the association's joinColumn - this will add On Delete Cascade to the foreign key column in the database:@ORM\JoinColumn(name="father_id", referencedColumnName="id", onDelete="CASCADE")
I also want to point out that the way you have your cascade={"remove"}
right now, if you delete a Child object, this cascade will remove the Parent object. Clearly not what you want.
Here is simple example. A contact has one to many associated phone numbers. When a contact is deleted, I want all its associated phone numbers to also be deleted, so I use ON DELETE CASCADE. The one-to-many/many-to-one relationship is implemented with by the foreign key in the phone_numbers.
CREATE TABLE contacts
(contact_id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(75) NOT NULL,
PRIMARY KEY(contact_id)) ENGINE = InnoDB;
CREATE TABLE phone_numbers
(phone_id BIGINT AUTO_INCREMENT NOT NULL,
phone_number CHAR(10) NOT NULL,
contact_id BIGINT NOT NULL,
PRIMARY KEY(phone_id),
UNIQUE(phone_number)) ENGINE = InnoDB;
ALTER TABLE phone_numbers ADD FOREIGN KEY (contact_id) REFERENCES \
contacts(contact_id) ) ON DELETE CASCADE;
By adding "ON DELETE CASCADE" to the foreign key constraint, phone_numbers will automatically be deleted when their associated contact is deleted.
INSERT INTO table contacts(name) VALUES('Robert Smith');
INSERT INTO table phone_numbers(phone_number, contact_id) VALUES('8963333333', 1);
INSERT INTO table phone_numbers(phone_number, contact_id) VALUES('8964444444', 1);
Now when a row in the contacts table is deleted, all its associated phone_numbers rows will automatically be deleted.
DELETE TABLE contacts as c WHERE c.id=1; /* delete cascades to phone_numbers */
To achieve the same thing in Doctrine, to get the same DB-level "ON DELETE CASCADE" behavoir, you configure the @JoinColumn with the onDelete="CASCADE" option.
<?php
namespace Entities;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Entity
* @Table(name="contacts")
*/
class Contact
{
/**
* @Id
* @Column(type="integer", name="contact_id")
* @GeneratedValue
*/
protected $id;
/**
* @Column(type="string", length="75", unique="true")
*/
protected $name;
/**
* @OneToMany(targetEntity="Phonenumber", mappedBy="contact")
*/
protected $phonenumbers;
public function __construct($name=null)
{
$this->phonenumbers = new ArrayCollection();
if (!is_null($name)) {
$this->name = $name;
}
}
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
}
public function addPhonenumber(Phonenumber $p)
{
if (!$this->phonenumbers->contains($p)) {
$this->phonenumbers[] = $p;
$p->setContact($this);
}
}
public function removePhonenumber(Phonenumber $p)
{
$this->phonenumbers->remove($p);
}
}
<?php
namespace Entities;
/**
* @Entity
* @Table(name="phonenumbers")
*/
class Phonenumber
{
/**
* @Id
* @Column(type="integer", name="phone_id")
* @GeneratedValue
*/
protected $id;
/**
* @Column(type="string", length="10", unique="true")
*/
protected $number;
/**
* @ManyToOne(targetEntity="Contact", inversedBy="phonenumbers")
* @JoinColumn(name="contact_id", referencedColumnName="contact_id", onDelete="CASCADE")
*/
protected $contact;
public function __construct($number=null)
{
if (!is_null($number)) {
$this->number = $number;
}
}
public function setPhonenumber($number)
{
$this->number = $number;
}
public function setContact(Contact $c)
{
$this->contact = $c;
}
}
?>
<?php
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
$contact = new Contact("John Doe");
$phone1 = new Phonenumber("8173333333");
$phone2 = new Phonenumber("8174444444");
$em->persist($phone1);
$em->persist($phone2);
$contact->addPhonenumber($phone1);
$contact->addPhonenumber($phone2);
$em->persist($contact);
try {
$em->flush();
} catch(Exception $e) {
$m = $e->getMessage();
echo $m . "<br />\n";
}
If you now do
# doctrine orm:schema-tool:create --dump-sql
you will see that the same SQL will be generated as in the first, raw-SQL example
While the proper way to delete on cascade is using @Michael Ridgway answer, there is also the possibility to listen do doctrine events to do the same thing.
Why ? Well you may want to do additional things when removing a father entity, maybe using a soft deleteable on some or hard removing others. You could also reaffect his children to another entity in case where you want to keep it and affect it to a parent entity etc...
So the way to do that would be to listen the doctrine event preRemove.
preRemove - The preRemove event occurs for a given entity before the respective EntityManager remove operation for that entity is executed. It is not called for a DQL DELETE statement.
Note that this event will be called only when using ->remove
.
Start by creating your event subscriber/listener to listen to this event:
<?php
namespace App\EventSubscriber;
use Doctrine\Common\EventSubscriber;
use App\Repository\FatherRepository;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use App\Entity\Father;
use App\Entity\Child;
class DoctrineSubscriber implements EventSubscriber
{
private $fatherRepository;
public function __construct(FatherRepository $fatherRepository)
{
$this->fatherRepository = $fatherRepository;
}
public function getSubscribedEvents(): array
{
return [
Events::preRemove => 'preRemove',
];
}
public function preRemove(LifecycleEventArgs $args)
{
$entity = $args->getObject();
if ($entity instanceof Father) {
//Custom code to handle children, for example reaffecting to another father:
$childs = $entity->getChildren();
foreach($childs as $child){
$otherFather = $this->fatherRepository->getOtherFather();
child->setFather($otherFather);
}
}
}
}
And don't forget to add this EventSubscriber your services.yaml
App\EventSubscriber\DoctrineSubscriber:
tags:
- { name: doctrine.event_subscriber }
In this example, the father will still be deleted but the children will not by having a new father. For example, if the entity Father
add other family members we could reaffect the children to someone else from the family.
精彩评论