Автоматическое удаление связанных строк в Laravel (Eloquent ORM)


158

Когда я удаляю строку, используя этот синтаксис:

$user->delete();

Есть ли способ прикрепить своего рода обратный вызов, чтобы он, например, делал это автоматически:

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

Желательно внутри модельного класса.

Ответы:


205

Я считаю, что это идеальный вариант использования событий Eloquent ( http://laravel.com/docs/eloquent#model-events ). Вы можете использовать событие «удаление» для очистки:

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

Вы, вероятно, должны также поместить все это в транзакцию, чтобы обеспечить ссылочную целостность.


7
Примечание: я провожу некоторое время, пока не заработал. Мне нужно было добавить first()в запрос, чтобы я мог получить доступ к модели-события, например, User::where('id', '=', $id)->first()->delete(); Источник
Мишель Айрес

6
@MichelAyres: да, вам нужно вызывать delete () для экземпляра модели, а не для Query Builder. У Builder есть собственный метод delete (), который в основном просто выполняет SQL-запрос DELETE, поэтому я предполагаю, что он ничего не знает о событиях
orm

3
Это способ мягкого удаления. Я считаю, что новый / предпочтительный способ Laravel состоит в том, чтобы вставить все это в метод boot () AppServiceProvider следующим образом: \ App \ User :: deleting (function ($ u) {$ u-> photos () -> delete ( );});
Водный Кайман

4
Почти работая в Laravel 5.5, мне пришлось добавить a foreach($user->photos as $photo), $photo->delete()чтобы убедиться, что у каждого ребенка были удалены его дети на всех уровнях, а не только на одном, как это происходило по какой-то причине.
Джордж

9
Это не каскад это дальше, хотя. Например, если Photosимеет tagsи вы делаете то же самое в Photosмодели (т.е. наdeleting методе:), $photo->tags()->delete();он никогда не получает триггер. Но если я сделаю это forциклом и сделаю что-то вроде for($user->photos as $photo) { $photo->delete(); }этого, то tagsтакже будет удален! просто к сведению
суперсан

200

Вы можете настроить это в своих миграциях:

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

Источник: http://laravel.com/docs/5.1/migrations#foreign-key-constraints

Вы также можете указать желаемое действие для свойств «при удалении» и «при обновлении» ограничения:

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

Да, я думаю, мне следовало уточнить эту зависимость.
Крис Шмитц

62
Но не в том случае, если вы используете мягкое удаление, поскольку строки не удаляются.
дрожь

7
Также - это удалит запись в БД, но не запустит ваш метод удаления, поэтому, если вы выполняете дополнительную работу по удалению (например - удаление файлов), она не запустится
amosmos

10
Этот подход основывается на БД для каскадного удаления, но не все БД поддерживают это, поэтому требуется дополнительная осторожность. Например, MySQL с механизмом MyISAM этого не делает, ни какие-либо NoSQL DB, SQLite в настройках по умолчанию и т. Д. Дополнительная проблема заключается в том, что artisan не будет предупреждать вас об этом при запуске миграций, он просто не будет создавать внешние ключи для таблиц MyISAM и при последующем удалении записи каскад не произойдет. Однажды у меня была эта проблема, и, поверьте мне, ее очень сложно отладить.
Иванхо

1
@kehinde Показанный вами подход НЕ вызывает события удаления в отношениях, которые будут удалены. Вы должны перебрать отношение и вызвать удаление по отдельности.
Том

51

Примечание : этот ответ был написан для Laravel 3 . Таким образом, может работать или не работать хорошо в более поздней версии Laravel.

Вы можете удалить все связанные фотографии перед тем, как фактически удалить пользователя.

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

Надеюсь, поможет.


1
Вы должны использовать: foreach ($ this-> photos как $ photo) ($ this-> photos вместо $ this-> photos ()) В противном случае, хороший совет!
Barryvdh

20
Чтобы сделать его более эффективным, используйте один запрос: Photo :: where ("user_id", $ this-> id) -> delete (); Не самый лучший способ, но только 1 запрос, способ повысить производительность, если у пользователя есть 1.000.000 фотографий.
Дирк

5
на самом деле вы можете позвонить: $ this-> photos () -> delete (); нет необходимости в петле - ivanhoe
ivanhoe

4
@ivanhoe Я заметил, что событие удаления не будет срабатывать на фотографии, если вы удалите коллекцию, однако, повторение, как предлагает Ахьяр, вызовет событие удаления. Это ошибка?
adamkrell

1
@akhyar Почти, вы можете сделать это с $this->photos()->delete(). photos()Возвращает объект конструктор запросов.
Свен ван Зелен

32

Отношение в модели пользователя:

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

Удалить запись и связанные:

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

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

$user->delete();

4
Это работает, но будьте осторожны, чтобы использовать $ user () -> ratio () -> detach (), если задействована сводная таблица (в случае отношений hasMany / ownToMany), иначе вы удалите ссылку, а не связь ,
Джеймс Бейли

Это работает для меня laravel 6. @Calin Вы можете объяснить больше пожалуйста?
Арман Х

20

Есть 3 подхода к решению этой проблемы:

1. Использование красноречивых событий при загрузке модели (ссылка: 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. Использование Eloquent Event Observers (ссылка: https://laravel.com/docs/5.7/eloquent#observers )

В вашем AppServiceProvider зарегистрируйте наблюдателя следующим образом:

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

Затем добавьте класс Observer следующим образом:

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

3. Использование ограничений внешнего ключа (ссылка: https://laravel.com/docs/5.7/migrations#foreign-key-constraints )

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

1
Я думаю, что 3 варианта являются наиболее элегантными, поскольку встраивает ограничение в саму базу данных. Я проверяю это и работает просто отлично.
Гилберт

14

Начиная с Laravel 5.2, в документации говорится, что обработчики событий такого типа должны быть зарегистрированы в AppServiceProvider:

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

Я даже предполагаю переместить их в отдельные классы вместо замыканий для лучшей структуры приложения.


1
Laravel 5.3 рекомендует помещать их в отдельные классы, называемые наблюдателями - хотя он документирован только в 5.3, Eloquent::observe()метод также доступен в 5.2 и может использоваться из AppServiceProvider.
Лейт

3
Если у вас есть какие - либо отношения «hasMany» от своего контекстуального photos(), вы также должны быть осторожны - этот процесс не УДАЛИТЬ внук , потому что вы не загружая модель. Вам нужно будет зациклить photos(заметьте, нет photos()) и запустить delete()метод на них как на моделях, чтобы инициировать события, связанные с удалением.
Лейт

1
@Leith Метод наблюдения также доступен в 5.1.
Тайлер Рид

2

Для этого лучше переопределить deleteметод. Таким образом, вы можете включить транзакции БД в сам deleteметод. Если вы используете способ события, вам придется deleteпри каждом вызове покрывать вызов метода транзакцией БД.

В твоей Userмодели.

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

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

    $result = parent::delete();

    \DB::commit();

    return $result;
}

1

В моем случае это было довольно просто, потому что мои таблицы базы данных - это InnoDB с внешними ключами с Cascade на Delete.

Таким образом, в этом случае, если ваша таблица фотографий содержит ссылку на внешний ключ для пользователя, все, что вам нужно сделать, это удалить отель, и очистка будет выполнена с помощью базы данных, база данных удалит все записи фотографий из данных. основание.


Как было отмечено в других Ответах, каскадное удаление на уровне базы данных не будет работать при использовании Soft Deletes. Предостережение для покупателя. :)
Бен Джонсон

1

Я бы перебрал всю коллекцию, отключив все перед удалением самого объекта.

вот пример:

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

Я знаю, что это не автоматически, но это очень просто.

Другой простой подход - предоставить модели метод. Как это:

public function detach(){
       try {

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

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

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

Тогда вы можете просто позвонить туда, где вам нужно:

$user->detach();
$user->delete();

0

Или вы можете сделать это, если хотите, просто другой вариант:

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

Обратите внимание, что если вы не используете соединение по умолчанию с laravel db, вам нужно сделать следующее:

DB::connection('connection_name')->pdo->beginTransaction();
DB::connection('connection_name')->pdo->commit();
DB::connection('connection_name')->pdo->rollBack();

0

Чтобы уточнить выбранный ответ, если ваши отношения также имеют дочерние отношения, которые необходимо удалить, вы должны сначала извлечь все записи дочерних отношений, а затем вызвать delete() метод, чтобы их события удаления также запускались правильно.

Вы можете сделать это легко с сообщениями более высокого порядка .

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

Вы также можете повысить производительность, запросив только столбец ID отношений:

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

да, но, как @supersan указал выше в комментарии, если вы удалите () в QueryBuilder, событие модели не будет запущено, потому что мы не загружаем саму модель, а затем вызываем delete () для этой модели.

События запускаются, только если мы используем функцию удаления в экземпляре модели.

Итак, это существо сказало:

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

чтобы удалить теги записей при удалении пользователя, нам нужно выполнить итерацию $user->postsи вызвать$post->delete()

foreach($user->posts as $post) { $post->delete(); } -> это вызовет событие удаления на пост

В.С.

$user->posts()->delete()-> это не сработает при удалении события post, потому что мы на самом деле не загружаем модель Post (мы запускаем только SQL-код: DELETE * from posts where user_id = $user->idтаким образом, модель Post даже не загружается)


-2

Вы можете использовать этот метод в качестве альтернативы.

Что произойдет, это то, что мы возьмем все таблицы, связанные с таблицей пользователей, и удалим связанные данные, используя цикл

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

Я не думаю, что все эти запросы необходимы, поскольку eloquent orm может справиться с этим, если вы укажете это четко.
7rust
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.