Po kasowaniu kaskadowym z doktryną 2

227

Próbuję zrobić prosty przykład, aby dowiedzieć się, jak usunąć wiersz z tabeli nadrzędnej i automatycznie usunąć pasujące wiersze w tabeli podrzędnej za pomocą Doctrine2.

Oto dwa elementy, których używam:

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

Tabele są poprawnie tworzone w bazie danych, ale opcja On Delete Cascade nie jest tworzona. Co ja robię źle?

rfc1484
źródło
Czy przetestowałeś już, czy kaskady działają poprawnie? Być może Doctrine obsługuje je w kodzie zamiast w bazie danych.
Problematyczne

Odpowiedzi:

408

Istnieją dwa rodzaje kaskad w Doctrine:

1) Poziom ORM - używa cascade={"remove"}w powiązaniu - jest to obliczenie, które jest wykonywane w UnitOfWork i nie wpływa na strukturę bazy danych. Po usunięciu obiektu UnitOfWork wykona iterację nad wszystkimi obiektami w powiązaniu i usunie je.

2) Poziom bazy danych - używa onDelete="CASCADE"na złączeniu asocjacji stowarzyszenia - spowoduje to dodanie opcji Usuń kaskadę do kolumny klucza obcego w bazie danych:

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

Chciałbym również zauważyć, że sposób, w jaki masz teraz kaskadę = {"remove"}, jeśli usuniesz obiekt potomny, ta kaskada usunie obiekt macierzysty. Wyraźnie nie to, czego chcesz.

Michael Ridgway
źródło
3
Ogólnie używam onDelete = "CASCADE", ponieważ oznacza to, że ORM musi wykonać mniej pracy i powinien mieć nieco lepszą wydajność.
Michael Ridgway,
57
Ja też, ale to zależy. Powiedzmy na przykład, że masz galerię zdjęć ze zdjęciami. Kiedy usuwasz galerię, chcesz, aby obrazy były również usuwane z dysku. Jeśli zaimplementujesz to w metodzie delete () obiektu obrazu, kaskadowe usuwanie za pomocą ORM sprawi, że wszystkie funkcje delte () twojego obrazu zostaną wywołane, oszczędzając ci pracy przy implementacji cronjobs, które sprawdzają osierocone pliki obrazów.
grypa
4
@ Michael Ridgway czasami należy zastosować obie instrukcje - onDeletejak cascade = {"remove"}na przykład, gdy masz jakiś obiekt związany z fosUser. Oba obiekty nie powinny istnieć same
Łukasz Adamczewski
17
Zauważ, że możesz po prostu pisać @ORM\JoinColumn(onDelete="CASCADE")i nadal pozwalać doktrynie automatycznie obsługiwać nazwy kolumn.
mcfedr
5
@dVaffection To dobre pytanie. Myślę, że onDelete="CASCADE"nie będzie to miało żadnego wpływu, ponieważ Doctrine's cascade={"remove"}usuwa powiązane elementy przed usunięciem elementu głównego (musi). Kiedy jednostka główna zostanie usunięta, nie ma już żadnych relacji zagranicznych onDelete="CASCADE"do usunięcia. Ale dla pewności zasugerowałbym, aby po prostu utworzyć mały przypadek testowy i spojrzeć na wykonywane zapytania i ich kolejność wykonywania.
grypa
50

Oto prosty przykład. Kontakt ma jeden do wielu powiązanych numerów telefonów. Kiedy kontakt zostanie usunięty, chcę, aby wszystkie powiązane z nim numery telefonów również zostały usunięte, więc używam ON DELETE CASCADE. Relacja jeden do wielu / wiele do jednego jest implementowana za pomocą klucza obcego w numerach telefonów.

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;

Po dodaniu „ON DELETE CASCADE” do ograniczenia klucza obcego numery telefonów zostaną automatycznie usunięte po usunięciu powiązanego z nimi kontaktu.

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

Teraz, gdy wiersz w tabeli kontaktów zostanie usunięty, wszystkie powiązane wiersze o numerach telefonów zostaną automatycznie usunięte.

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

Aby osiągnąć to samo w Doctrine, aby uzyskać to samo zachowanie na poziomie DB „ON DELETE CASCADE”, skonfiguruj @JoinColumn z opcją 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";
}

Jeśli teraz to zrobisz

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

zobaczysz, że zostanie wygenerowany taki sam SQL, jak w pierwszym przykładzie surowego SQL

Kurt Krueckeberg
źródło
4
Czy to jest prawidłowe umieszczenie? Usunięcie numeru telefonu nie powinno usuwać kontaktu. Jest to kontakt, którego usunięcie powinno uruchomić kaskadę. Dlaczego więc umieszczać kaskadę na dziecku / telefonie?
przemo_li
1
@przemo_li To jest prawidłowe umieszczenie. Kontakt nie wie, że istnieją numery telefonów, ponieważ numery telefonów mają odniesienie do kontaktu, a kontakt nie ma odniesienia do numerów telefonów. Jeśli więc kontakt zostanie usunięty, numer telefonu zawiera odniesienie do nieistniejącego kontaktu. W tym przypadku chcemy, aby coś się wydarzyło: wywołanie akcji ON DELETE. Postanowiliśmy skasować usunięcie, aby usunąć również numery telefonów.
marijnz0r
3
@przemi_li onDelete="cascade"jest poprawnie umieszczony w bycie (na dziecku), ponieważ tak jest kaskadowanie SQL , które jest umieszczane na potomnym. Jedynie kaskadowanie Doctrine ( cascade=["remove"]które nie jest tutaj używane) jest umieszczane na obiekcie nadrzędnym.
Maurice