На каскаде удаления с доктриной2

227

Я пытаюсь сделать простой пример, чтобы узнать, как удалить строку из родительской таблицы и автоматически удалить совпадающие строки в дочерней таблице с помощью Doctrine2.

Вот две сущности, которые я использую:

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

Таблицы правильно созданы в базе данных, но опция «Удалить каскад» не создается. Что я делаю не так?

rfc1484
источник
Вы проверяли, правильно ли работают каскады? Возможно, Doctrine обрабатывает их в коде, а не в базе данных.
Проблемное

Ответы:

408

В Учении есть два вида каскадов:

1) Уровень ORM - используется cascade={"remove"}в ассоциации - это расчет, который выполняется в UnitOfWork и не влияет на структуру базы данных. Когда вы удаляете объект, UnitOfWork будет перебирать все объекты в ассоциации и удалять их.

2) Уровень базы данных - используется onDelete="CASCADE"для joinColumn ассоциации - это добавит Каскад при удалении к столбцу внешнего ключа в базе данных:

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

Я также хочу отметить, что если у вас сейчас есть ваш каскад = {"remove"}, то при удалении дочернего объекта этот каскад удалит родительский объект. Понятно не то, что вы хотите.

Майкл Риджуэй
источник
3
Я обычно использую onDelete = "CASCADE", потому что это означает, что ORM должен выполнять меньше работы и иметь немного лучшую производительность.
Майкл Риджуэй
58
Я тоже, но это зависит. Скажем, например, у вас есть галерея изображений с изображениями. При удалении галереи вы хотите, чтобы изображения также удалялись с диска. Если вы реализуете это в методе delete () вашего объекта изображения, каскадное удаление с использованием ORM обеспечит вызов всех функций delte () вашего изображения, что избавит вас от необходимости выполнять cronjobs, которые проверяют потерянные файлы изображений.
грипп
4
@Michael Риджуэй иногда оба утверждения должны применяться - onDeleteтак же как cascade = {"remove"}, например , когда у вас есть какой - то объект , связанный с fosUser. Оба объекта не должны существовать в одиночку
Лука Адамчевски
17
Обратите внимание, что вы можете просто написать @ORM\JoinColumn(onDelete="CASCADE")и все же позволить доктрине автоматически обрабатывать имена столбцов.
МакФедр
5
@dVaffection Это хороший вопрос. Я думаю, что это onDelete="CASCADE"не будет иметь никакого эффекта, так как Doctrine cascade={"remove"}удаляет связанные сущности перед удалением корневой сущности (это необходимо). Таким образом, когда корневая сущность удаляется, не остается никаких внешних связей для onDelete="CASCADE"удаления. Но чтобы быть уверенным, я бы предложил вам создать небольшой тестовый пример и посмотреть на выполняемые запросы и порядок их выполнения.
грипп
50

Вот простой пример. У контакта есть один-много связанных телефонных номеров. Когда контакт удален, я хочу, чтобы все связанные с ним номера телефонов также были удалены, поэтому я использую ON DELETE CASCADE. Отношение «один ко многим / многие к одному» реализуется с помощью внешнего ключа в 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;

При добавлении «ON DELETE CASCADE» к ограничению внешнего ключа номера телефона будут автоматически удаляться при удалении связанного с ними контакта.

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

Теперь, когда строка в таблице контактов удалена, все связанные с ней строки phone_numbers будут автоматически удалены.

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

Чтобы добиться того же в Doctrine, чтобы получить тот же уровень поведения «ON DELETE CASCADE» на уровне базы данных, вы конфигурируете @JoinColumn с параметром 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";
}

Если вы сейчас делаете

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

вы увидите, что будет сгенерирован тот же SQL, что и в первом примере с raw-SQL

Курт Крюкеберг
источник
4
Это правильное размещение? Удаление номера телефона не должно удалять контакт. Это контакт, который удаление должно вызвать каскад. Зачем тогда ставить каскад на ребенка / телефон?
przemo_li
1
@przemo_li Это правильное размещение. Контакт не знает, что телефонные номера существуют, потому что телефонные номера имеют ссылку на контакт, а контакт не имеет ссылки на телефонные номера. Таким образом, если контакт удаляется, номер телефона имеет ссылку на несуществующий контакт. В этом случае мы хотим, чтобы что-то произошло: запуск действия ON DELETE. Мы решили каскадировать удаление, чтобы удалить и телефонные номера.
marijnz0r
3
@przemi_li onDelete="cascade"правильно помещается в сущность (на дочернем объекте), потому что это SQL-каскад , который размещается на дочернем объекте . Только каскад Доктрины ( cascade=["remove"]который здесь не используется) помещается на родительский элемент.
Морис