Exclusão automática de linhas relacionadas no Laravel (Eloquent ORM)

158

Quando eu excluo uma linha usando esta sintaxe:

$user->delete();

Existe uma maneira de anexar um tipo de retorno de chamada, para que, por exemplo, faça isso automaticamente:

$this->photo()->delete();

De preferência dentro da classe de modelo.

Martti Laine
fonte

Respostas:

205

Acredito que este é um caso de uso perfeito para eventos Eloquent ( http://laravel.com/docs/eloquent#model-events ). Você pode usar o evento "deletando" para fazer a limpeza:

class User extends Eloquent
{
    public function photos()
    {
        return $this->has_many('Photo');
    }

    // this is a recommended way to declare event handlers
    public static function boot() {
        parent::boot();

        static::deleting(function($user) { // before delete() method call this
             $user->photos()->delete();
             // do the rest of the cleanup...
        });
    }
}

Você provavelmente também deve colocar tudo dentro de uma transação, para garantir a integridade referencial.

ivanhoe
fonte
7
Nota: passo algum tempo até conseguir esse trabalho. Eu precisava para adicionar first()na consulta para que eu pudesse acessar o modelo de evento eg User::where('id', '=', $id)->first()->delete(); Fonte
Michel Ayres
6
@MichelAyres: sim, você precisa chamar delete () em uma instância de modelo, não no Query Builder. Builder tem o seu próprio método delete () que basicamente apenas executa uma consulta sql delete, então eu presumo que não sabe nada sobre eventos ORM ...
Ivanhoe
3
Este é o caminho a seguir para exclusões virtuais. Acredito que a nova / preferida maneira do Laravel é colar tudo isso no método boot () do AppServiceProvider desta maneira: \ App \ User :: deleting (function ($ u) {$ u-> photos () -> delete ( );});
precisa saber é o seguinte
4
Quase trabalhando no Laravel 5.5, eu tive que adicionar um foreach($user->photos as $photo), $photo->delete()para garantir que cada criança tivesse seus filhos removidos em todos os níveis, em vez de apenas um, como estava acontecendo por algum motivo.
George
9
Isso não faz mais cascata ainda. Por exemplo, se Photoshas tagse você faz o mesmo no Photosmodelo (isto é, no deletingmétodo $photo->tags()->delete();:), ele nunca é acionado. Mas se eu fizer um forloop e fazer algo parecido for($user->photos as $photo) { $photo->delete(); }, o tagstambém será excluído! apenas FYI
supersan
200

Você pode configurá-lo nas suas migrações:

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

Fonte: http://laravel.com/docs/5.1/migrations#foreign-key-constraints

Você também pode especificar a ação desejada para as propriedades "na exclusão" e "na atualização" da restrição:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
Chris Schmitz
fonte
Sim, acho que deveria ter esclarecido essa dependência.
22713 Chris Chrismitmit
62
Mas não se você estiver usando exclusões dinâmicas, pois as linhas não serão realmente excluídas.
tremby
7
Além disso - o que irá excluir o registro no DB, mas não vai executar o seu método de exclusão, por isso, se você está fazendo trabalho extra de exclusão (por exemplo - apagar arquivos), ele não será executado
amosmos
10
Essa abordagem depende do banco de dados para excluir em cascata, mas nem todos os bancos de dados suportam isso, portanto, é necessário um cuidado extra. Por exemplo, o MySQL com o mecanismo MyISAM não possui, nem nenhum banco de dados NoSQL, SQLite na configuração padrão, etc. Problema adicional é que o artesão não avisa sobre isso quando você executa migrações, mas não cria chaves estrangeiras nas tabelas MyISAM e quando você excluir um registro posteriormente, nenhuma cascata acontecerá. Tive esse problema uma vez e, acredite, é muito difícil depurar.
ivanhoe
1
@kehinde A abordagem mostrada por você NÃO invoca eventos de exclusão nas relações a serem excluídas. Você deve iterar sobre a relação e chamar a exclusão individualmente.
Tom
51

Nota : Esta resposta foi escrita para o Laravel 3 . Assim, pode ou não funcionar bem na versão mais recente do Laravel.

Você pode excluir todas as fotos relacionadas antes de excluir o usuário.

<?php

class User extends Eloquent
{

    public function photos()
    {
        return $this->has_many('Photo');
    }

    public function delete()
    {
        // delete all related photos 
        $this->photos()->delete();
        // as suggested by Dirk in comment,
        // it's an uglier alternative, but faster
        // Photo::where("user_id", $this->id)->delete()

        // delete the user
        return parent::delete();
    }
}

Espero que ajude.

akhy
fonte
1
Você tem que usar: foreach ($ this-> fotos como $ photo) ($ this-> fotos em vez de $ this-> fotos ()) Caso contrário, boa dica!
Barryvdh
20
Para torná-lo mais eficiente, use uma consulta: Photo :: where ("user_id", $ this-> id) -> delete (); Não é a maneira mais agradável, mas apenas uma consulta, um desempenho muito melhor se um usuário tiver 1.000.000 de fotos.
Dirk
5
na verdade, você pode chamar: $ this-> photos () -> delete (); Não há necessidade de loop - ivanhoe
ivanhoe
4
@ivanhoe Notei que o evento de exclusão não será acionado na foto se você excluir a coleção; no entanto, a iteração conforme akhyar sugere fará com que o evento de exclusão seja acionado. Isso é um inseto?
adamkrell
1
@akhyar Quase, você pode fazer isso $this->photos()->delete(). O photos()retorna o objeto construtor de consulta.
Sven van Zoelen
32

Relação no modelo de usuário:

public function photos()
{
    return $this->hasMany('Photo');
}

Excluir registro e relacionados:

$user = User::find($id);

// delete related   
$user->photos()->delete();

$user->delete();
Calin Blaga
fonte
4
Isso funciona, mas tenha cuidado para usar $ user () -> relacionamento () -> desanexar () se houver uma tabela dinâmica envolvida (no caso de relações hasMany / belongsToMany), caso contrário você excluirá a referência, não a relação .
James Bailey
Isso funciona para mim laravel 6. @Calin, você pode explicar mais pls?
Arman H
20

Existem 3 abordagens para resolver isso:

1. Usando eventos eloquentes na inicialização do modelo (ref: https://laravel.com/docs/5.7/eloquent#events )

class User extends Eloquent
{
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->delete();
        });
    }
}

2. Usando observadores de eventos eloquentes (ref: https://laravel.com/docs/5.7/eloquent#observers )

No seu AppServiceProvider, registre o observador da seguinte maneira:

public function boot()
{
    User::observe(UserObserver::class);
}

Em seguida, adicione uma classe Observer da seguinte forma:

class UserObserver
{
    public function deleting(User $user)
    {
         $user->photos()->delete();
    }
}

3. Usando restrições de chave estrangeira (ref: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
fonte
1
Eu acho que as 3 opções são as mais elegantes, pois estão construindo a restrição no próprio banco de dados. Eu testo e funciona muito bem.
Gilbert
14

A partir do Laravel 5.2, a documentação afirma que esses tipos de manipuladores de eventos devem ser registrados no AppServiceProvider:

<?php
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        User::deleting(function ($user) {
            $user->photos()->delete();
        });
    }

Suponho até movê-los para classes separadas, em vez de encerramentos, para uma melhor estrutura de aplicativos.

Attila Fulop
fonte
1
O Laravel 5.3 recomenda colocá-los em classes separadas chamadas Observadores - embora apenas seja documentado no 5.3, o Eloquent::observe()método também está disponível no 5.2 e pode ser usado no AppServiceProvider.
Leith
3
Se você tiver quaisquer relações 'hasMany' de seu photos(), você também precisa ter cuidado - este processo não netos de exclusão, porque você está não carregar modelos. Você precisará fazer um loop photos(note, não photos()) e acionar o delete()método neles como modelos para acionar os eventos relacionados à exclusão.
11186 Leith
1
@Leith O método observe também está disponível no 5.1.
Tyler Reed
2

É melhor se você substituir o deletemétodo para isso. Dessa forma, você pode incorporar transações de banco de dados no deletepróprio método. Se você usar o caminho do evento, terá que cobrir sua chamada de deletemétodo com uma transação de banco de dados toda vez que a chamar.

No seu Usermodelo.

public function delete()
{
    \DB::beginTransaction();

     $this
        ->photo()
        ->delete()
    ;

    $result = parent::delete();

    \DB::commit();

    return $result;
}
Ranga Lakshitha
fonte
1

No meu caso, foi bem simples, porque minhas tabelas de banco de dados são InnoDB com chaves estrangeiras com Cascade on Delete.

Portanto, nesse caso, se sua tabela de fotos contiver uma referência de chave estrangeira para o usuário, tudo o que você precisará fazer é excluir o hotel e a limpeza será feita pelo Banco de Dados, o banco de dados excluirá todos os registros de fotos dos dados base.

Alex
fonte
Como foi observado em outras respostas, as exclusões em cascata na camada do banco de dados não funcionarão ao usar exclusões dinâmicas. Comprador, cuidado. :)
Ben Johnson
1

Iria percorrer a coleção desanexando tudo antes de excluir o próprio objeto.

aqui está um exemplo:

try {
        $user = user::findOrFail($id);
        if ($user->has('photos')) {
            foreach ($user->photos as $photo) {

                $user->photos()->detach($photo);
            }
        }
        $user->delete();
        return 'User deleted';
    } catch (Exception $e) {
        dd($e);
    }

Eu sei que não é automático, mas é muito simples.

Outra abordagem simples seria fornecer ao modelo um método. Como isso:

public function detach(){
       try {

            if ($this->has('photos')) {
                foreach ($this->photos as $photo) {

                    $this->photos()->detach($photo);
                }
            }

        } catch (Exception $e) {
            dd($e);
        }
}

Em seguida, você pode simplesmente chamar isso onde precisar:

$user->detach();
$user->delete();
Carlos A. Carneiro
fonte
0

Ou você pode fazer isso se quiser, apenas outra opção:

try {
    DB::connection()->pdo->beginTransaction();

    $photos = Photo::where('user_id', '=', $user_id)->delete(); // Delete all photos for user
    $user = Geofence::where('id', '=', $user_id)->delete(); // Delete users

    DB::connection()->pdo->commit();

}catch(\Laravel\Database\Exception $e) {
    DB::connection()->pdo->rollBack();
    Log::exception($e);
}

Observe que se você não estiver usando a conexão laravel db padrão, precisará fazer o seguinte:

DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();
Darren Powers
fonte
0

Para elaborar a resposta selecionada, se seus relacionamentos também tiverem relacionamentos filhos que devem ser excluídos, você deverá recuperar todos os registros de relacionamentos filhos primeiro e depois chamar o delete()método para que seus eventos de exclusão também sejam acionados corretamente.

Você pode fazer isso facilmente com mensagens de ordem superior .

class User extends Eloquent
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->get()->each->delete();
        });
    }
}

Você também pode melhorar o desempenho consultando apenas a coluna ID do relacionamento:

class User extends Eloquent
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    public static function boot() {
        parent::boot();

        static::deleting(function($user) {
             $user->photos()->get(['id'])->each->delete();
        });
    }
}
Steve Bauman
fonte
-1

Sim, mas como o @supersan afirmou acima em um comentário, se você excluir () em um QueryBuilder, o evento do modelo não será acionado, porque não estamos carregando o modelo em si, e chamamos delete () nesse modelo.

Os eventos são disparados apenas se usarmos a função de exclusão em uma Instância do Modelo.

Então, este ser disse:

if user->hasMany(post)
and if post->hasMany(tags)

para excluir as tags de postagem ao excluir o usuário, teríamos que repetir $user->postse chamar$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> isso acionará o evento de exclusão no Post

VS

$user->posts()->delete()-> isso não dispara o evento de exclusão na publicação porque, na verdade, não carregamos o Post Model (apenas executamos um SQL como: DELETE * from posts where user_id = $user->ide, portanto, o modelo Post nem sequer é carregado)

reconquistar
fonte
-2

Você pode usar esse método como uma alternativa.

O que acontecerá é que pegamos todas as tabelas associadas à tabela de usuários e excluímos os dados relacionados usando loop

$tables = DB::select("
    SELECT
        TABLE_NAME,
        COLUMN_NAME,
        CONSTRAINT_NAME,
        REFERENCED_TABLE_NAME,
        REFERENCED_COLUMN_NAME
    FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
    WHERE REFERENCED_TABLE_NAME = 'users'
");

foreach($tables as $table){
    $table_name =  $table->TABLE_NAME;
    $column_name = $table->COLUMN_NAME;

    DB::delete("delete from $table_name where $column_name = ?", [$id]);
}
Daanzel
fonte
Não acho que todas essas consultas sejam necessárias, pois a eloquent orm pode lidar com isso se você especificar claramente.
7rust