При каскадном удалении с помощью doctrine2

#php #doctrine-orm #symfony #cascading-deletes

#php #doctrine-orm #symfony #каскадирование-удаляет

Вопрос:

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

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

Child.php:

 <?php

namespace AcmeCascadeBundleEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 * @ORMTable(name="child")
 */
class Child {

    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @ORMManyToOne(targetEntity="Father", cascade={"remove"})
     *
     * @ORMJoinColumns({
     *   @ORMJoinColumn(name="father_id", referencedColumnName="id")
     * })
     *
     * @var father
     */
    private $father;
}
  

Father.php

 <?php
namespace AcmeCascadeBundleEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 * @ORMTable(name="father")
 */
class Father
{
    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="AUTO")
     */
    private $id;
}
  

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

Комментарии:

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

Ответ №1:

В Doctrine есть два вида каскадов:

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

  2. Уровень базы данных — использует onDelete="CASCADE" в объединенном столбце ассоциации — это добавит каскадное удаление к столбцу внешнего ключа в базе данных:

     @ORMJoinColumn(name="father_id", referencedColumnName="id", onDelete="CASCADE")
      

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

Комментарии:

1. Обычно я использую OnDelete=»CASCADE», потому что это означает, что ORM должен выполнять меньше работы и у него должна быть немного лучшая производительность.

2. Я тоже так делаю, но это зависит. Допустим, например, у вас есть галерея изображений с изображениями. Когда вы удаляете галерею, вы хотите, чтобы изображения также удалялись с диска. Если вы реализуете это в методе delete() вашего объекта image, каскадное удаление с использованием ORM гарантирует, что все функции delte () вашего изображения будут вызваны, избавляя вас от работы по внедрению cronjobs, которые проверяют наличие потерянных файлов изображений.

3. @Michael Ridgway иногда следует применять оба оператора — onDelete а также cascade = {"remove"} , например, когда у вас есть какой-либо объект, связанный с FOSUser. Оба объекта не должны существовать по отдельности

4. Обратите внимание, что вы можете просто написать @ORMJoinColumn(onDelete="CASCADE") и все равно позволить doctrine обрабатывать имена столбцов автоматически.

5. @dVaffection Это хороший вопрос. Я думаю, что это onDelete="CASCADE" не окажет никакого влияния, поскольку Doctrine cascade={"remove"} удаляет связанные объекты перед удалением корневого объекта (это необходимо). Таким образом, когда корневой объект удаляется, не остается никаких внешних связей для onDelete="CASCADE" удаления. Но чтобы быть уверенным, я бы посоветовал вам просто создать небольшой тестовый пример и посмотреть на выполняемые запросы и их порядок выполнения.

Ответ №2:

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

Путем добавления «ПРИ КАСКАДНОМ УДАЛЕНИИ» к ограничению внешнего ключа, phone_numbers будут автоматически удалены, когда связанный с ними контакт будет удален.
удалено.

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

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

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

Чтобы добиться того же, что и в Doctrine, для получения того же поведения «ПРИ КАСКАДНОМ удалении» на уровне базы данных, вы настраиваете @JoinColumn с помощью
опция OnDelete=»КАСКАД».

 <?php
namespace Entities;

use DoctrineCommonCollectionsArrayCollection;

/**
 * @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 = DoctrineORMEntityManager::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

Комментарии:

1. Правильное ли это размещение? Удаление номера телефона не должно приводить к удалению контакта. Это контакт, удаление которого должно вызвать каскад. Зачем тогда размещать каскад на дочернем устройстве / телефоне?

2. @przemo_li Это правильное размещение. Контакт не знает о существовании телефонных номеров, потому что телефонные номера имеют ссылку на контакт, а у контакта нет ссылки на телефонные номера. Таким образом, если контакт удаляется, номер телефона имеет ссылку на несуществующий контакт. В этом случае мы хотим, чтобы что-то произошло: запуск действия ПРИ УДАЛЕНИИ. Мы решили выполнить каскадное удаление, то есть также удалить телефонные номера.

3. @przemi_li onDelete="cascade" правильно помещен в объект (на дочернем объекте), потому что это каскадный SQL , который размещается на дочернем объекте. В родительский файл помещается только каскадное удаление Doctrine ( cascade=["remove"] которое не используется здесь).

Ответ №3:

Хотя правильный способ удаления при каскадном удалении — использовать ответ @Michael Ridgway, существует также возможность прослушивать события do doctrine, чтобы сделать то же самое.

Почему? Ну, вы можете захотеть выполнить дополнительные действия при удалении родительской сущности, возможно, используя мягкое удаление для некоторых или жесткое удаление других. Вы также можете повторно привязать его дочерние элементы к другому объекту в случае, если вы хотите сохранить его и привязать к родительскому объекту и т.д…

Таким образом, способ сделать это — прослушать предварительное удаление события doctrine.

Предварительное удаление — Событие предварительного удаления происходит для данного объекта до выполнения соответствующей операции удаления EntityManager для этого объекта. Он не вызывается для инструкции DQL DELETE.

Обратите внимание, что это событие будет вызываться только при использовании ->remove .

Начните с создания вашего подписчика / прослушивателя события для прослушивания этого события:

 <?php

namespace AppEventSubscriber;

use DoctrineCommonEventSubscriber;
use AppRepositoryFatherRepository;
use DoctrinePersistenceEventLifecycleEventArgs;
use AppEntityFather;
use AppEntityChild;

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

И не забудьте добавить это событие в список ваших сервисов.yaml

   AppEventSubscriberDoctrineSubscriber:
    tags:
      - { name: doctrine.event_subscriber }
  

В этом примере отец все равно будет удален, но дочерние элементы не будут, поскольку у них будет новый отец. Например, если объект Father добавит других членов семьи, мы могли бы повторно передать дочерних элементов кому-то еще из семьи.