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.

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ść.


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

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.


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

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

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.


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

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.


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

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

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

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


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

Nie sądzę, aby wszystkie te zapytania były konieczne, ponieważ elokwentny orm poradzi sobie z tym, jeśli jasno to określisz.
— 7
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.