Na exclusão em cascata com a doutrina2

227

Estou tentando fazer um exemplo simples para aprender como excluir uma linha de uma tabela pai e excluir automaticamente as linhas correspondentes na tabela filho usando o Doctrine2.

Aqui estão as duas entidades que estou usando:

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

As tabelas foram criadas corretamente no banco de dados, mas a opção Ao excluir cascata não é criada. O que estou fazendo de errado?

rfc1484
fonte
Você já testou se as cascatas funcionam corretamente? Talvez o Doctrine lide com eles no código, e não no banco de dados.
Problema

Respostas:

408

Existem dois tipos de cascatas no Doctrine:

1) Nível ORM - usa cascade={"remove"}na associação - este é um cálculo feito no UnitOfWork e não afeta a estrutura do banco de dados. Quando você remove um objeto, o UnitOfWork itera sobre todos os objetos da associação e os remove.

2) Nível do banco de dados - usa onDelete="CASCADE"no joinColumn da associação - isso adicionará Ao excluir cascata à coluna de chave estrangeira no banco de dados:

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

Também quero ressaltar que, da maneira que você tem sua cascata = {"remove"} agora, se você excluir um objeto filho, essa cascata removerá o objeto pai. Claramente não é o que você quer.

Michael Ridgway
fonte
3
Eu geralmente uso onDelete = "CASCADE" porque significa que o ORM precisa fazer menos trabalho e deve ter um desempenho um pouco melhor.
Michael Ridgway
58
Eu também, mas depende. Digamos, por exemplo, que você tenha uma galeria de imagens com imagens. Quando você exclui a galeria, também deseja que as imagens sejam excluídas do disco. Se você implementar isso no método delete () do seu objeto de imagem, a exclusão em cascata usando o ORM garantirá que todas as funções delte () de sua imagem sejam chamadas, poupando o trabalho de implementar cronjobs que verificam arquivos de imagem órfãos.
flu
4
@ Michael Ridgway às vezes as duas instruções devem ser aplicadas - onDeleteassim como, cascade = {"remove"}por exemplo, quando você tem algum objeto relacionado ao fosUser. Ambos os objetos não devem existir sozinhos
Luke Adamczewski
17
Observe que você pode escrever @ORM\JoinColumn(onDelete="CASCADE")e ainda permitir que a doutrina lide com os nomes das colunas automaticamente.
Mcfedr
5
@dVaffection Essa é uma boa pergunta. Eu acho que onDelete="CASCADE"isso não terá nenhum efeito, pois o Doctrine cascade={"remove"}remove as entidades relacionadas antes de remover a entidade raiz (é necessário). Portanto, quando a entidade raiz é excluída, não há mais relações externas onDelete="CASCADE"a serem excluídas. Mas para ter certeza, sugiro que você simplesmente crie um pequeno caso de teste e observe as consultas que estão sendo executadas e sua ordem de execução.
flu
50

Aqui está um exemplo simples. Um contato tem de um a muitos números de telefone associados. Quando um contato é excluído, desejo que todos os seus números de telefone associados também sejam excluídos, portanto, uso ON DELETE CASCADE. O relacionamento um para muitos / muitos para um é implementado com a chave estrangeira nos números de telefone.

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;

Ao adicionar "ON DELETE CASCADE" à restrição de chave estrangeira, os números de telefone serão excluídos automaticamente quando o contato associado for excluído.

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

Agora, quando uma linha na tabela de contatos é excluída, todas as suas linhas de número de telefone associadas serão excluídas automaticamente.

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

Para obter o mesmo resultado no Doctrine, para obter o mesmo comportamento "ON DELETE CASCADE" no nível do banco de dados, configure o @JoinColumn com a opção 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";
}

Se você faz agora

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

você verá que o mesmo SQL será gerado como no primeiro exemplo bruto de SQL

Kurt Krueckeberg
fonte
4
É o posicionamento correto? A exclusão do número de telefone não deve excluir o contato. É o contato com quem a exclusão deve acionar a cascata. Por que colocar cascata na criança / telefone?
Przemo_li #
1
@przemo_li É o posicionamento correto. O contato não sabe que existem números de telefone, porque os números de telefone têm uma referência ao contato e um contato não tem uma referência aos números de telefone. Portanto, se um contato for excluído, um número de telefone fará referência a um contato inexistente. Nesse caso, queremos que algo aconteça: acionando a ação ON DELETE. Decidimos fazer a exclusão em cascata, para excluir também os números de telefone.
Marijnz0r
3
@przemi_li onDelete="cascade"é colocado corretamente na entidade (no filho) porque é o SQL em cascata , que é colocado no filho. Somente a Doutrina em cascata ( cascade=["remove"]que não é usada aqui) é colocada no pai.
Maurice