261

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?

rfc1484
  • 9,441
  • 16
  • 72
  • 123
  • Have you tested whether the cascades perform correctly anyway? Perhaps Doctrine handles them in code instead of in database. – Problematic Jun 13 '11 at 16:19

3 Answers3

461

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.

Michael Ridgway
  • 5,259
  • 1
  • 16
  • 6
  • Thanks! I think I understand now how to do it properly. Which one of the cascade methods do you consider best practice or would you recommend? – rfc1484 Jun 13 '11 at 20:05
  • 5
    I generally use onDelete="CASCADE" because it means the ORM has to do less work and it should have a little bit better performance. – Michael Ridgway Jun 16 '11 at 15:44
  • 66
    I do too but it depends. Say for example you have an image gallery with images. When you delete the gallery you want the images to be deleted from disk too. If you implement that in the delete() method of your image object the cascading delete using the ORM will make sure that all your image's delte() functions are called, saving you the work of implementing cronjobs that check for orphaned image files. – flu Mar 21 '12 at 10:09
  • 4
    @Michael Ridgway sometimes both statements should be applied - `onDelete` as well as `cascade = {"remove"}` for example when You have some object related with fosUser. Both objects should not exist alone – Luke Adamczewski Aug 29 '12 at 08:13
  • +1 for the illuminating answer. I'd only have stressed a bit more that the cascade={remove} in the question is on the wrong side. – Paolo Stefan Jun 11 '14 at 13:11
  • It's not work in my case. I tryed with `onDelete="SET NULL"` and its work now :) – Petroff Jun 18 '14 at 07:09
  • 24
    Note that you can just write `@ORM\JoinColumn(onDelete="CASCADE")` and still let doctrine handle the column names automatically. – mcfedr Oct 20 '14 at 10:30
  • 1
    * @OneToMany(targetEntity="Comment", mappedBy="author", cascade={"persist", "remove"}); – Buddy Mar 24 '15 at 11:37
  • 1
    @flu Can I use both `cascade={"remove"}` and `onDelete="CASCADE"`? Will lifecycle events still be triggered on child rows? – Dmitry Dec 03 '15 at 18:57
  • 6
    @dVaffection That's a good question. I think that the `onDelete="CASCADE"` won't have any affect since Doctrine's `cascade={"remove"}` removes the related entities before removing the root entity (it has to). So when the root entity is deleted there aren't any foreign relations left for `onDelete="CASCADE"` to delete. But to be sure I would suggest you simply create a small test case and look at the queries being executed and their order of execution. – flu Dec 04 '15 at 10:14
  • @flu Nice catch, though it *will* have an effect if other system not using doctrine, like a web-service or your Navicat also modifies the database. So if you remove it through doctrine you will have a graceful proper deletion but if you do it by other means it won't leave "orphans" :P – xDaizu Nov 24 '16 at 11:11
  • 1
    ... and to make onDelete="CASCADE" work, we will need to make a migration – Az.Youness May 17 '19 at 17:35
53

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

Katrina
  • 1,922
  • 2
  • 24
  • 42
Kurt Krueckeberg
  • 1,225
  • 10
  • 11
  • 4
    Is it correct placement? Deleting phone number should not delete contact. It's contact whom deletion should trigger cascade. Why then place cascade on child/phone? – przemo_li Nov 09 '15 at 10:52
  • 1
    @przemo_li It is correct placement. The contact doesn't know phone numbers exist, because the phone numbers have a reference to the contact, and a contact doesn't have a reference to the phone numbers. So if a contact gets deleted, a phone number has a reference to a non-existing contact. In this case, we want something to happen: triggering the ON DELETE action. We decided to cascade the deletion, so to delete the phone numbers as well. – marijnz0r May 11 '16 at 08:16
  • 3
    @przemi_li the `onDelete="cascade"` is placed correctly in the entity (on the child) because that is **SQL cascading**, which is placed on the child. Only the Doctrine cascading (`cascade=["remove"]`, which is *not* used here) is placed on the parent. – Maurice Oct 06 '16 at 11:21
0

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.

Dylan KAS
  • 4,840
  • 2
  • 15
  • 33