On delete cascade with doctrine2 On delete cascade with doctrine2 symfony symfony

On delete cascade with doctrine2


There are two kinds of cascades in Doctrine:

1) ORM level - uses cascade={"remove"} in the association - this is a calculation that is done in the UnitOfWork and does not affect the database structure. When you remove an object, the UnitOfWork will iterate over all objects in the association and remove them.

2) 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 numbersto 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 isdeleted.

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 withthe onDelete="CASCADE" option.

<?phpnamespace 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);    }}<?phpnamespace 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 therespective 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:

<?phpnamespace 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.