Automatyczne usuwanie powiązanych wierszy w Laravel (Eloquent ORM)

158

Kiedy usuwam wiersz przy użyciu tej składni:

$user->delete();

Czy istnieje sposób na dołączenie jakiegoś wywołania zwrotnego, aby np. Robił to automatycznie:

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

Najlepiej wewnątrz klasy model.

Martti Laine
źródło

Odpowiedzi:

205

Uważam, że jest to doskonały przypadek użycia dla wydarzeń Eloquent ( http://laravel.com/docs/eloquent#model-events ). Możesz użyć zdarzenia „deleting”, aby wyczyścić:

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

Prawdopodobnie powinieneś również umieścić całość wewnątrz transakcji, aby zapewnić referencyjną integralność.

ivanhoe
źródło
7
Uwaga: spędzam trochę czasu, zanim to zadziała. Musiałem dodać first()do zapytania, aby uzyskać dostęp do zdarzenia modelu, np. User::where('id', '=', $id)->first()->delete(); Źródło
Michel Ayres.
6
@MichelAyres: tak, musisz wywołać metodę delete () w instancji modelu, a nie w Query Builder. Builder ma własną metodę delete (), która po prostu uruchamia zapytanie DELETE sql, więc zakładam, że nie wie nic o zdarzeniach orm ...
ivanhoe
3
To jest sposób na miękkie usuwanie. Uważam, że nowym / preferowanym sposobem Laravela jest umieszczenie wszystkich tych elementów w metodzie boot () AppServiceProvider w ten sposób: \ App \ User :: deleting (function ($ u) {$ u-> photos () -> delete ( );});
Watercayman
4
Prawie pracowałem w Laravel 5.5, musiałem dodać a foreach($user->photos as $photo), a następnie $photo->delete()upewnić się, że każde dziecko ma usunięte swoje dzieci na wszystkich poziomach, zamiast tylko jednego, jak to się dzieje z jakiegoś powodu.
George
9
Nie powoduje to jednak dalszego rozwoju. Na przykład, jeśli Photosma tagsi zrobisz to samo w Photosmodelu (tj. W deletingmetodzie $photo->tags()->delete();:), nigdy nie zostanie wyzwolony. Ale jeśli zrobię forpętlę i zrobię coś takiego, for($user->photos as $photo) { $photo->delete(); }to tagsrównież zostaną usunięte! tylko do Twojej wiadomości
supersan
199

Możesz to ustawić w swoich migracjach:

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

Źródło: http://laravel.com/docs/5.1/migrations#foreign-key-constraints

Możesz także określić żądane działanie dla właściwości „przy usuwaniu” i „przy aktualizacji” ograniczenia:

$table->foreign('user_id')
      ->references('id')->on('users')
      ->onDelete('cascade');
Chris Schmitz
źródło
Tak, chyba powinienem był wyjaśnić tę zależność.
Chris Schmitz
62
Ale nie, jeśli używasz usuwania nietrwałego, ponieważ wiersze nie są wtedy naprawdę usuwane.
tremby
7
Ponadto - spowoduje to usunięcie rekordu w bazie danych, ale nie uruchomi metody usuwania, więc jeśli wykonujesz dodatkową pracę przy usuwaniu (na przykład - usuwasz pliki), nie będzie działać
amosmos
10
To podejście polega na usuwaniu kaskadowym przez bazę danych, ale nie wszystkie bazy danych to obsługują, dlatego wymagana jest dodatkowa ostrożność. Na przykład MySQL z silnikiem MyISAM nie, ani żadne bazy danych NoSQL, SQLite w domyślnej konfiguracji itp. Dodatkowym problemem jest to, że rzemieślnik nie ostrzeże Cię o tym podczas uruchamiania migracji, po prostu nie utworzy kluczy obcych w tabelach MyISAM i kiedy później usuniesz rekord, żadna kaskada nie nastąpi. Miałem kiedyś ten problem i uwierz mi, bardzo trudno go debugować.
ivanhoe
1
@kehinde Przedstawione przez Ciebie podejście NIE wywołuje zdarzeń usunięcia relacji, które mają zostać usunięte. Powinieneś iterować relację i indywidualnie wywoływać usuwanie.
Tom
51

Uwaga : ta odpowiedź została napisana dla Laravel 3 . Zatem może, ale nie musi, działać dobrze w nowszej wersji Laravel.

Możesz usunąć wszystkie powiązane zdjęcia przed faktycznym usunięciem użytkownika.

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

Mam nadzieję, że to pomoże.

akhy
źródło
1
Musisz użyć: foreach ($ this-> photos as $ photo) ($ this-> photos zamiast $ this-> photos ()) W przeciwnym razie dobra wskazówka!
Barryvdh
20
Aby uczynić to bardziej wydajnym, użyj jednego zapytania: Photo :: where ("user_id", $ this-> id) -> delete (); Nie jest to najmilszy sposób, ale tylko 1 zapytanie, znacznie lepsza wydajność, jeśli użytkownik ma 1.000.000 zdjęć.
Dirk
5
właściwie możesz zadzwonić: $ this-> photos () -> delete (); no need for loop - ivanhoe
ivanhoe
4
@ivanhoe Zauważyłem, że zdarzenie usuwające nie zostanie uruchomione na zdjęciu, jeśli usuniesz kolekcję, jednak iteracja zgodnie z sugestią akhyar spowoduje uruchomienie zdarzenia usuwania. Czy to błąd?
adamkrell,
1
@akhyar Prawie możesz to zrobić $this->photos()->delete(). photos()Zwraca obiekt kreator zapytań.
Sven van Zoelen
32

Relacja w modelu użytkownika:

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

Usuń rekord i powiązane:

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

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

$user->delete();
Calin Blaga
źródło
4
To działa, ale uważaj, aby użyć $ user () -> relations () -> detach (), jeśli jest zaangażowana tabela przestawna (w przypadku relacji hasMany / contribToMany), w przeciwnym razie usuniesz odniesienie, a nie relację .
James Bailey
To działa dla mnie laravel 6. @Calin czy możesz wyjaśnić więcej pls?
Arman H
19

Istnieją 3 sposoby rozwiązania tego problemu:

1. Używanie Eloquent Events On Model Boot (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. Korzystanie z Eloquent Event Observers (patrz: https://laravel.com/docs/5.7/eloquent#observers )

W swoim AppServiceProvider zarejestruj obserwatora w następujący sposób:

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

Następnie dodaj klasę Observer w następujący sposób:

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

3. Korzystanie z ograniczeń klucza obcego (patrz: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Paras
źródło
1
Myślę, że te 3 opcje są najbardziej eleganckie, ponieważ wbudowują ograniczenie w samą bazę danych. Testuję to i działa dobrze.
Gilbert
14

Począwszy od Laravel 5.2, dokumentacja stwierdza, że tego typu programy obsługi zdarzeń powinny być zarejestrowane w AppServiceProvider:

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

Przypuszczam nawet, że przeniosę je do oddzielnych klas zamiast domknięć dla lepszej struktury aplikacji.

Attila Fulop
źródło
1
Laravel 5.3 zaleca umieszczanie ich w oddzielnych klasach zwanych Observers - chociaż jest to udokumentowane tylko w 5.3, Eloquent::observe()metoda jest również dostępna w wersji 5.2 i może być używana z AppServiceProvider.
Leith
3
Jeśli masz jakieś relacje „hasMany” ze swojego photos(), musisz również zachować ostrożność - ten proces nie usunie wnuków, ponieważ nie ładujesz modeli. Będziesz musiał zapętlić photos(uwaga, nie photos()) i uruchomić delete()na nich metodę jako modele, aby uruchomić zdarzenia związane z usuwaniem.
Leith
1
@Leith Metoda obserwacji jest również dostępna w wersji 5.1.
Tyler Reed
2

Lepiej będzie, jeśli zmienisz tę deletemetodę. W ten sposób możesz włączyć transakcje DB do deletesamej metody. Jeśli korzystasz ze sposobu zdarzenia, będziesz musiał pokrywać wywołanie deletemetody transakcją DB za każdym razem, gdy ją wywołasz.

W Twoim Usermodelu.

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}
Ranga Lakshitha
źródło
1

W moim przypadku było to całkiem proste, ponieważ moje tabele bazy danych to InnoDB z kluczami obcymi z Cascade on Delete.

Więc w tym przypadku, jeśli twoja tabela zdjęć zawiera odniesienie do klucza obcego dla użytkownika, to wszystko, co musisz zrobić, to usunąć hotel, a czyszczenie zostanie wykonane przez Bazę Danych, baza danych usunie wszystkie rekordy zdjęć z danych baza.

Alex
źródło
Jak zauważono w innych odpowiedziach, kaskadowe usuwanie w warstwie bazy danych nie będzie działać, gdy używasz usuwania miękkiego. Kupujący, strzeż się. :)
Ben Johnson
1

Iterowałbym przez kolekcję, odłączając wszystko przed usunięciem samego obiektu.

oto przykład:

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

Wiem, że to nie jest automatyczne, ale jest bardzo proste.

Innym prostym podejściem byłoby dostarczenie modelowi metody. Lubię to:

public function detach(){
       try {

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

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

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

Następnie możesz po prostu zadzwonić tam, gdzie potrzebujesz:

$user->detach();
$user->delete();
Carlos A. Carneiro
źródło
0

Lub możesz to zrobić, jeśli chcesz, po prostu inna opcja:

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

Uwaga, jeśli nie używasz domyślnego połączenia laravel db, musisz wykonać następujące czynności:

DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();
Darren Powers
źródło
0

Aby uszczegółowić wybraną odpowiedź, jeśli Twoje relacje mają również związki podrzędne, które należy usunąć, musisz najpierw pobrać wszystkie rekordy relacji podrzędnych, a następnie zadzwonić do delete() metodę, aby zdarzenia usuwania również zostały poprawnie .

Możesz to łatwo zrobić z wiadomościami wyższego rzędu .

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

Możesz również poprawić wydajność, wysyłając zapytania tylko do kolumny ID relacji:

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
źródło
-1

tak, ale jak @supersan stwierdził u góry w komentarzu, jeśli usuniesz () w QueryBuilder, zdarzenie modelu nie zostanie uruchomione, ponieważ nie ładujemy samego modelu, a następnie wywołujemy metodę delete () w tym modelu.

Zdarzenia są uruchamiane tylko wtedy, gdy używamy funkcji usuwania na wystąpieniu modelu.

Więc to powiedział:

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

aby usunąć tagi postów podczas usuwania użytkownika, musielibyśmy powtórzyć $user->postsi zadzwonić$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> to uruchomi zdarzenie usuwania w Post

VS

$user->posts()->delete()-> to nie uruchomi zdarzenia kasującego w poście, ponieważ tak naprawdę nie ładujemy Post Modelu (uruchamiamy tylko SQL typu: DELETE * from posts where user_id = $user->ida zatem model Post nie jest nawet ładowany)

rechim
źródło
-2

Możesz użyć tej metody jako alternatywy.

To, co się stanie, to pobranie wszystkich tabel powiązanych z tabelą użytkowników i usunięcie powiązanych danych za pomocą zapętlenia

$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
źródło
Nie sądzę, aby wszystkie te zapytania były konieczne, ponieważ elokwentny orm poradzi sobie z tym, jeśli jasno to określisz.
7