Suppression automatique des lignes associées dans Laravel (Eloquent ORM)

158

Lorsque je supprime une ligne en utilisant cette syntaxe:

$user->delete();

Existe-t-il un moyen d'attacher une sorte de rappel, de sorte qu'il le fasse par exemple automatiquement:

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

De préférence à l'intérieur de la classe modèle.

Martti Laine
la source

Réponses:

205

Je pense que c'est un cas d'utilisation parfait pour les événements Eloquent ( http://laravel.com/docs/eloquent#model-events ). Vous pouvez utiliser l'événement "suppression" pour effectuer le nettoyage:

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

Vous devriez probablement aussi mettre le tout dans une transaction, pour garantir l'intégrité référentielle.

Ivanhoe
la source
7
Remarque: je passe du temps jusqu'à ce que cela fonctionne. J'avais besoin d'ajouter first()dans la requête pour pouvoir accéder au modèle-événement, par exemple User::where('id', '=', $id)->first()->delete(); Source
Michel Ayres
6
@MichelAyres: oui, vous devez appeler delete () sur une instance de modèle, pas sur Query Builder. Builder a sa propre méthode delete () qui exécute simplement une requête DELETE sql, donc je suppose qu'il ne sait rien des événements orm ...
ivanhoe
3
C'est la voie à suivre pour les suppressions logicielles. Je crois que la nouvelle / préférée de Laravel est de coller tout cela dans la méthode boot () d'AppServiceProvider de cette manière: \ App \ User :: deleting (function ($ u) {$ u-> photos () -> delete ( );});
Watercayman
4
Presque travaillé dans Laravel 5.5, j'ai dû ajouter un foreach($user->photos as $photo), puis $photo->delete()pour m'assurer que chaque enfant avait ses enfants supprimés à tous les niveaux, au lieu d'un seul comme cela se passait pour une raison quelconque.
George
9
Cependant, cela ne le fait pas en cascade davantage. Par exemple, si Photosa tagset vous faites la même chose dans le Photosmodèle (c'est-à-dire sur la deletingméthode :), $photo->tags()->delete();il ne se déclenche jamais. Mais si je fais une forboucle et faire quelque chose comme for($user->photos as $photo) { $photo->delete(); }alors le tagsfaire aussi supprimé! just FYI
supersan
200

Vous pouvez en fait configurer cela dans vos migrations:

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

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

Vous pouvez également spécifier l'action souhaitée pour les propriétés «à la suppression» et «à la mise à jour» de la contrainte:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
Chris Schmitz
la source
Ouais, je suppose que j'aurais dû clarifier cette dépendance.
Chris Schmitz
62
Mais pas si vous utilisez des suppressions logicielles, car les lignes ne sont pas vraiment supprimées.
tremby
7
De plus - cela supprimera l'enregistrement dans la base de données, mais n'exécutera pas votre méthode de suppression, donc si vous effectuez un travail supplémentaire sur la suppression (par exemple - supprimer des fichiers), il ne fonctionnera pas
amosmos
10
Cette approche repose sur DB pour effectuer la suppression en cascade, mais tous les DB ne le prennent pas en charge, donc une attention particulière est requise. Par exemple, MySQL avec le moteur MyISAM ne le fait pas, ni aucune base de données NoSQL, SQLite dans la configuration par défaut, etc. lorsque vous supprimerez ultérieurement un enregistrement, aucune cascade ne se produira. J'ai eu ce problème une fois et croyez-moi, c'est très difficile à déboguer.
ivanhoe
1
@kehinde L'approche que vous avez indiquée n'invoque PAS les événements de suppression sur les relations à supprimer. Vous devez parcourir la relation et appeler delete individuellement.
Tom
51

Remarque : Cette réponse a été écrite pour Laravel 3 . Ainsi pourrait ou pourrait ne pas fonctionner correctement dans la version plus récente de Laravel.

Vous pouvez supprimer toutes les photos associées avant de supprimer réellement l'utilisateur.

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

J'espère que ça aide.

akhy
la source
1
Vous devez utiliser: foreach ($ this-> photos as $ photo) ($ this-> photos au lieu de $ this-> photos ()) Sinon, bon conseil!
Barryvdh
20
Pour le rendre plus efficace, utilisez une requête: Photo :: where ("user_id", $ this-> id) -> delete (); Ce n'est pas la meilleure façon, mais une seule requête, bien meilleure performance si un utilisateur a 1.000.000 de photos.
Dirk
5
en fait, vous pouvez appeler: $ this-> photos () -> delete (); pas besoin de boucle - ivanhoe
ivanhoe
4
@ivanhoe J'ai remarqué que l'événement de suppression ne se déclenchera pas sur la photo si vous supprimez la collection, cependant, l'itération comme le suggère akhyar provoquera le déclenchement de l'événement de suppression. Est-ce un bug?
adamkrell
1
@akhyar Presque, vous pouvez le faire avec $this->photos()->delete(). Le photos()renvoie l'objet de création de requête.
Sven van Zoelen
32

Relation dans le modèle utilisateur:

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

Supprimer l'enregistrement et associé:

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

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

$user->delete();
Calin Blaga
la source
4
Cela fonctionne, mais faites attention à utiliser $ user () -> relation () -> detach () s'il y a une table piviot impliquée (dans le cas des relations hasMany / appartientToMany), sinon vous supprimerez la référence, pas la relation .
James Bailey
Cela fonctionne pour moi laravel 6. @Calin pouvez-vous expliquer plus de pls?
Arman H le
20

Il existe 3 approches pour résoudre ce problème:

1. Utilisation des événements éloquents sur le modèle de démarrage (réf: 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. Utilisation des observateurs d'événements éloquents (réf: https://laravel.com/docs/5.7/eloquent#observers )

Dans votre AppServiceProvider, enregistrez l'observateur comme ceci:

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

Ensuite, ajoutez une classe Observer comme ceci:

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

3. Utilisation des contraintes de clé étrangère (réf: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
la source
1
Je pense que les 3 options sont les plus élégantes puisque la construction de la contrainte dans la base de données elle-même. Je le teste et fonctionne très bien.
Gilbert
14

Depuis Laravel 5.2, la documentation indique que ces types de gestionnaires d'événements doivent être enregistrés dans AppServiceProvider:

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

Je suppose même de les déplacer vers des classes séparées au lieu de fermetures pour une meilleure structure d'application.

Attila Fulop
la source
1
Laravel 5.3 recommande de les placer dans des classes séparées appelées Observers - bien qu'elle ne soit documentée que dans 5.3, la Eloquent::observe()méthode est également disponible dans la version 5.2 et peut être utilisée à partir de l'AppServiceProvider.
Leith
3
Si vous avez des relations «hasMany» de votre photos(), vous devrez également faire attention - ce processus ne supprimera pas les petits-enfants car vous ne chargez pas de modèles. Vous devrez boucler photos(notez, non photos()) et lancer la delete()méthode sur eux en tant que modèles afin de déclencher les événements liés à la suppression.
Leith
1
@Leith La méthode observe est également disponible en 5.1.
Tyler Reed
2

Il vaut mieux remplacer la deleteméthode pour cela. De cette façon, vous pouvez incorporer des transactions DB dans la deleteméthode elle-même. Si vous utilisez la voie événementielle, vous devrez couvrir votre appel dedelete méthode avec une transaction DB à chaque fois que vous l'appelez.

Dans votre Usermodèle.

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}
Ranga Lakshitha
la source
1

Dans mon cas, c'était assez simple car mes tables de base de données sont InnoDB avec des clés étrangères avec Cascade on Delete.

Donc, dans ce cas, si votre table de photos contient une référence de clé étrangère pour l'utilisateur, il vous suffit de supprimer l'hôtel et le nettoyage sera effectué par la base de données, la base de données supprimera tous les enregistrements de photos des données. base.

Alex
la source
Comme indiqué dans d'autres réponses, les suppressions en cascade au niveau de la couche de base de données ne fonctionneront pas lors de l'utilisation des suppressions logicielles. Attention l'acheteur. :)
Ben Johnson
1

Je voudrais parcourir la collection en détachant tout avant de supprimer l'objet lui-même.

voici un exemple:

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

Je sais que ce n'est pas automatique mais c'est très simple.

Une autre approche simple consisterait à fournir au modèle une méthode. Comme ça:

public function detach(){
       try {

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

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

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

Ensuite, vous pouvez simplement appeler ceci là où vous avez besoin:

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

Ou vous pouvez le faire si vous le souhaitez, juste une autre option:

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

Notez que si vous n'utilisez pas la connexion par défaut de laravel db, vous devez effectuer les opérations suivantes:

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

Pour élaborer sur la réponse sélectionnée, si vos relations ont également des relations enfants qui doivent être supprimées, vous devez d'abord récupérer tous les enregistrements de relations enfants, puis appeler le delete() méthode afin que leurs événements de suppression soient également déclenchés correctement.

Vous pouvez le faire facilement avec des messages d'ordre supérieur .

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

Vous pouvez également améliorer les performances en interrogeant uniquement la colonne ID des relations:

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
la source
-1

oui, mais comme @supersan l'a indiqué en haut dans un commentaire, si vous supprimez () sur un QueryBuilder, l'événement de modèle ne sera pas déclenché, car nous ne chargeons pas le modèle lui-même, puis appelons delete () sur ce modèle.

Les événements sont déclenchés uniquement si nous utilisons la fonction de suppression sur une instance de modèle.

Donc, cet être dit:

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

afin de supprimer les balises de publication lors de la suppression de l'utilisateur, nous devrons parcourir $user->postset appeler$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> cela déclenchera l'événement de suppression sur Post

CONTRE

$user->posts()->delete()-> cela ne déclenchera pas l'événement de suppression lors de la publication car nous ne chargeons pas réellement le modèle de publication (nous exécutons uniquement un SQL comme: DELETE * from posts where user_id = $user->idet donc, le modèle de publication n'est même pas chargé)

rechim
la source
-2

Vous pouvez utiliser cette méthode comme alternative.

Ce qui va se passer, c'est que nous prenons toutes les tables associées à la table users et supprimons les données associées en utilisant la boucle

$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
la source
Je ne pense pas que toutes ces requêtes soient nécessaires car eloquent orm peut gérer cela si vous le spécifiez clairement.
7