ChatGPT解决这个技术问题 Extra ChatGPT

On delete cascade with doctrine2

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?

Have you tested whether the cascades perform correctly anyway? Perhaps Doctrine handles them in code instead of in database.

D
DonCallisto

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.


I generally use onDelete="CASCADE" because it means the ORM has to do less work and it should have a little bit better performance.
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.
@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
Note that you can just write @ORM\JoinColumn(onDelete="CASCADE") and still let doctrine handle the column names automatically.
@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.
K
Katrina

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


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 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.
@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.
D
Dylan Kas

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.


关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now