En cas de suppression de cascade avec doctrine2

227

J'essaie de faire un exemple simple afin d'apprendre à supprimer une ligne d'une table parent et à supprimer automatiquement les lignes correspondantes dans la table enfant à l'aide de Doctrine2.

Voici les deux entités que j'utilise:

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;
}

Les tables sont correctement créées dans la base de données, mais l'option Sur supprimer la cascade n'est pas créée. Qu'est-ce que je fais mal?

rfc1484
la source
Avez-vous testé si les cascades fonctionnent correctement de toute façon? Peut-être que Doctrine les gère dans le code plutôt que dans la base de données.
Problématique

Réponses:

408

Il existe deux types de cascades dans Doctrine:

1) Niveau ORM - utilise cascade={"remove"}dans l'association - c'est un calcul qui est fait dans UnitOfWork et n'affecte pas la structure de la base de données. Lorsque vous supprimez un objet, UnitOfWork parcourt tous les objets de l'association et les supprime.

2) Niveau base de données - utilise onDelete="CASCADE"sur la colonne joinColumn de l'association - cela ajoutera On Delete Cascade à la colonne de clé étrangère dans la base de données:

@ORM\JoinColumn(name="father_id", referencedColumnName="id", onDelete="CASCADE")

Je tiens également à souligner que la façon dont vous avez votre cascade = {"supprimer"} en ce moment, si vous supprimez un objet enfant, cette cascade supprimera l'objet parent. Ce n'est clairement pas ce que vous voulez.

Michael Ridgway
la source
3
J'utilise généralement onDelete = "CASCADE" car cela signifie que l'ORM doit faire moins de travail et qu'il devrait avoir des performances un peu meilleures.
Michael Ridgway
58
Moi aussi, mais cela dépend. Disons par exemple que vous avez une galerie d'images avec des images. Lorsque vous supprimez la galerie, vous souhaitez également que les images soient supprimées du disque. Si vous implémentez cela dans la méthode delete () de votre objet image, la suppression en cascade à l'aide de l'ORM s'assurera que toutes les fonctions delte () de votre image sont appelées, vous épargnant ainsi le travail d'implémentation de cronjobs qui vérifient les fichiers image orphelins.
grippe
4
@Michael Ridgway parfois les deux instructions doivent être appliquées - onDeleteainsi que cascade = {"remove"}par exemple lorsque vous avez un objet lié à fosUser. Les deux objets ne devraient pas exister seuls
Luke Adamczewski
17
Notez que vous pouvez simplement écrire @ORM\JoinColumn(onDelete="CASCADE")et laisser la doctrine gérer automatiquement les noms de colonne.
mcfedr du
5
@dVaffection C'est une bonne question. Je pense que cela onDelete="CASCADE"n'aura aucun effet puisque Doctrine cascade={"remove"}supprime les entités liées avant de supprimer l'entité racine (il le doit). Ainsi, lorsque l'entité racine est supprimée, il ne reste plus de relations étrangères onDelete="CASCADE"à supprimer. Mais pour être sûr, je vous suggère de simplement créer un petit cas de test et de regarder les requêtes en cours d'exécution et leur ordre d'exécution.
grippe
50

Voici un exemple simple. Un contact a un à plusieurs numéros de téléphone associés. Lorsqu'un contact est supprimé, je souhaite que tous ses numéros de téléphone associés soient également supprimés, donc j'utilise ON DELETE CASCADE. La relation un-à-plusieurs / plusieurs-à-un est implémentée par la clé étrangère dans les numéros de téléphone.

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;

En ajoutant "ON DELETE CASCADE" à la contrainte de clé étrangère, les numéros de téléphone seront automatiquement supprimés lorsque leur contact associé sera supprimé.

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);

Désormais, lorsqu'une ligne du tableau des contacts est supprimée, toutes les lignes associées de phone_numbers seront automatiquement supprimées.

DELETE TABLE contacts as c WHERE c.id=1; /* delete cascades to phone_numbers */

Pour obtenir la même chose dans Doctrine, pour obtenir le même comportement "ON DELETE CASCADE" au niveau de la base de données, vous configurez @JoinColumn avec l' option onDelete = "CASCADE" .

<?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";
}

Si vous le faites maintenant

# doctrine orm:schema-tool:create --dump-sql

vous verrez que le même SQL sera généré comme dans le premier exemple raw-SQL

Kurt Krueckeberg
la source
4
Est-ce un placement correct? La suppression du numéro de téléphone ne doit pas supprimer le contact. C'est le contact dont la suppression devrait déclencher la cascade. Pourquoi alors placer la cascade sur l'enfant / le téléphone?
przemo_li
1
@przemo_li C'est un placement correct. Le contact ne connaît pas les numéros de téléphone, car les numéros de téléphone ont une référence au contact et un contact n'a pas de référence aux numéros de téléphone. Donc, si un contact est supprimé, un numéro de téléphone fait référence à un contact inexistant. Dans ce cas, nous voulons que quelque chose se produise: déclencher l'action ON DELETE. Nous avons décidé de supprimer la suppression en cascade, afin de supprimer également les numéros de téléphone.
marijnz0r
3
@przemi_li le onDelete="cascade"est placé correctement dans l'entité (sur l'enfant) car il s'agit de la cascade SQL , qui est placée sur l'enfant. Seule la Doctrine en cascade ( cascade=["remove"], qui n'est pas utilisée ici) est placée sur le parent.
Maurice