Введение
Представьте, что пользователь случайно удалил важную запись в блоге. Или администратор очистил таблицу с данными, которые нужны для квартального отчёта. В обычной ситуации эти данные исчезли бы навсегда. Но что, если бы у нас была страховочная сетка? Возможность сказать: «Не волнуйтесь, данные всё ещё здесь, мы можем их восстановить».
TL;DR: Soft Delete в двух словах
Soft delete (мягкое удаление) — механизм, который помечает записи как удалённые через колонку deleted_at, но физически оставляет их в базе данных.
Быстрый старт:
// 1. В модели
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model {
use SoftDeletes;
}
// 2. В миграции
$table->softDeletes();
// 3. Использование
$post->delete(); // мягко удалить
$post->restore(); // восстановить
$post->forceDelete(); // удалить навсегда
Post::withTrashed()->get(); // все, включая удалённые
Post::onlyTrashed()->get(); // только удалённыеВажно: не заменяет аудит, требует внимания к GDPR. Подробности — ниже.
Soft delete (мягкое удаление) — это именно такая сетка. Вместо того чтобы безвозвратно стирать строки из базы данных, мы просто помечаем их как «удалённые». Сами записи остаются на месте, но при обычных запросах приложение их игнорирует. Это даёт нам пространство для манёвра: мы можем восстановить данные, если потребуется, или удалить их по-настоящему позже, когда убедимся, что они точно не нужны.
Laravel делает работу с мягким удалением на удивление простой. Добавили один трейт в модель, создали миграцию с нужным полем — и всё, система готова. Но за этой простотой скрывается множество нюансов: от правильной работы с запросами до тестирования и автоматической очистки старых записей.
В этом руководстве мы собрали всё, что нужно знать о soft delete в Laravel. Разберёмся, как это работает под капотом, когда стоит использовать мягкое удаление, а когда от него лучше отказаться, и как выжать максимум из встроенных возможностей фреймворка. Материал получился объёмным, но вы всегда можете использовать оглавление, чтобы перейти к интересующему разделу.
Что такое мягкое удаление (Soft Delete)
В самом простом виде soft delete — это подход к удалению данных, при котором запись физически остаётся в базе данных, но помечается как удалённая. В Laravel для этого используется столбец deleted_at с типом timestamp. Если в этом поле стоит дата и время — запись считается удалённой. Если там NULL — запись активна.
Удобнее всего представить это как корзину на вашем компьютере. Когда вы удаляете файл, он исчезает из папки, но физически продолжает существовать на диске и может быть восстановлен. Примерно то же самое происходит и с мягко удалёнными записями в базе: они не видны в обычных запросах, но при необходимости их можно «достать из корзины» и вернуть обратно.
Как это выглядит в коде? Допустим, у нас есть модель Post и мы хотим удалить запись с ID 1:
$post = Post::find(1);
$post->delete();При обычном (жёстком) удалении эта команда просто стёрла бы строку из таблицы posts. Но если модель использует soft delete, Laravel выполнит примерно такой SQL-запрос:
UPDATE posts SET deleted_at = '2024-02-16 11:26:49' WHERE id = 1То есть данные остались на месте, изменилось только одно поле.
Теперь, когда мы делаем обычный запрос Post::all(), Laravel автоматически добавляет условие WHERE deleted_at IS NULL, и мягко удалённые записи не попадают в результаты. Это поведение обеспечивается глобальной областью видимости Illuminate\Database\Eloquent\SoftDeletingScope, которую фреймворк регистрирует при использовании трейта SoftDeletes.
Преимущества и недостатки использования Soft Delete
Прежде чем мы углубимся в код и настройку, важно понять, когда soft delete действительно нужен, а когда он может создать больше проблем, чем решить. Как и любой инструмент, мягкое удаление хорошо в одних ситуациях и не очень — в других. Давайте честно разберём обе стороны медали.
Преимущества
Восстановление данных
Это, пожалуй, самое очевидное преимущество. Ошибки случаются — пользователь может случайно удалить важную запись, администратор — очистить не ту таблицу. С soft delete у вас всегда есть запасной план. Вместо того чтобы лезть в бекапы (если они есть) или прощаться с данными навсегда, вы просто выполняете $post->restore(), и запись снова в строю.
Аудит данных без лишних телодвижений
Иногда данные нужно сохранять даже после "удаления" — для аналитики, отчётов или соответствия регуляторным требованиям. Например, интернет-магазину может потребоваться хранить информацию о заказах удалённого пользователя для налоговой отчётности. Soft delete позволяет держать такие данные в той же таблице, просто помечая их как неактивные.
Двухэтапное удаление
В некоторых сценариях полезно добавить дополнительный уровень проверки перед окончательным удалением. Например, менеджер может отправить пользователя в "корзину", а администратор — либо подтвердить безвозвратное удаление, либо восстановить. Это даёт подстраховку от поспешных решений и удобно реализуется именно через мягкое удаление.
Недостатки и подводные камни
Рост базы данных
Звучит очевидно, но об этом легко забыть: удалённые записи никуда не деваются. Они продолжают занимать место. В небольших проектах это незаметно, но если вы ежедневно мягко удаляете тысячи записей, через пару лет объём данных может стать проблемой. Хорошая новость: мы обсудим, как с этим бороться, в разделе про очистку старых записей.
Риск случайно включить "удалённые" данные в запросы
Когда вы работаете через Eloquent, фреймворк автоматически добавляет условие WHERE deleted_at IS NULL ко всем запросам. Но если вы пишете сырые SQL-запросы или используете фасад DB, этой защиты нет. И вот тут легко ошибиться:
// Eloquent - безопасно, мягко удалённые не попадут
$activePosts = Post::where('category_id', 5)->get();
// DB фасад - опасная зона, вернёт всё, включая удалённые
$allPostsRaw = DB::table('posts')->where('category_id', 5)->get();Это не значит, что DB фасад — зло. Просто нужно помнить о нюансе и при необходимости добавлять условие ->whereNull('deleted_at') вручную.
Вопросы конфиденциальности и соответствия GDPR
Это тонкий момент. Представьте: пользователь из Евросоюза запросил удаление своих данных. Вы делаете soft delete, помечая запись как удалённую. Формально данные остались в базе. Не нарушает ли это GDPR?
Ответ зависит от того, что именно вы храните и как долго. Если речь о персональных данных, которые должны быть удалены по запросу пользователя, просто поставить флаг deleted_at недостаточно для соответствия GDPR. Вместо этого вам нужно либо полностью удалить строку (forceDelete), либо анонимизировать персональные данные, заменив реальные имя и email на хеш или заглушку (deleted_user@example.com)..
Soft delete — это не журнал аудита
Иногда к soft delete относятся как к способу отследить, кто и когда что удалил. Да, столбец deleted_at показывает время "удаления", но этого катастрофически мало для полноценного аудита. Кто удалил запись? Почему? Что было в ней до удаления? На эти вопросы мягкое удаление не отвечает. Для таких задач нужны специализированные решения (пакеты вроде spatie/laravel-activitylog или кастомные логи).
Пошаговая настройка мягкого удаления
Теперь, когда мы разобрались с теорией, перейдём к практике. Настроить мягкое удаление в Laravel на удивление просто — потребуется буквально два шага. Давайте пройдём их вместе.
Для примеров будем использовать модель Post — типичную сущность для блога или новостного сайта.
Добавляем трейт SoftDeletes в модель
Откройте файл модели (app/Models/Post.php) и подключите трейт Illuminate\Database\Eloquent\SoftDeletes:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Post extends Model
{
use HasFactory, SoftDeletes;
// остальной код модели
}
Этот трейт даёт несколько вещей:
- Методы для работы с мягким удалением:
delete(),restore(),forceDelete(),trashed(). - Глобальный скоуп, который автоматически исключает мягко удалённые записи из всех запросов.
- События модели, связанные с удалением/восстановлением (
deleting,deleted,restoring,restored).
Создаём миграцию для поля deleted_at
Теперь нужно добавить в таблицу posts колонку deleted_at. Laravel предоставляет удобный метод softDeletes() для этого.
Создадим миграцию через Artisan:
php artisan make:migration add_deleted_at_to_posts_table --table=postsВ сгенерированном файле миграции добавим вызов softDeletes() в метод up() и не забудем про откат в методе down():
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->softDeletes(); // добавляет колонку deleted_at типа timestamp
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropSoftDeletes(); // удаляет колонку deleted_at
});
}
};
Метод softDeletes() создаёт обычную колонку deleted_at с типом TIMESTAMP и значением NULL по умолчанию. Это идеально подходит для наших целей.
Для проектов, где важна временная зона.
Laravel также предоставляет метод softDeletesTz(). Он создаёт колонку deleted_at с поддержкой временной зоны (timestamp with time zone или аналог, в зависимости от СУБД).
Schema::table('posts', function (Blueprint $table) {
$table->softDeletesTz(); // колонка deleted_at с поддержкой временной зоны
});Используйте этот вариант, если:
- Ваше приложение работает с пользователями в разных часовых поясах.
- Сервер базы данных настроен на работу с UTC, а приложение — на локальное время.
- Требуется повышенная точность аудита событий.
В методе down() для удаления используется dropSoftDeletesTz():
Schema::table('posts', function (Blueprint $table) {
$table->dropSoftDeletesTz();
});Для большинства проектов достаточно стандартного softDeletes(), но знать о такой возможности полезно.
Выполняем миграцию
Выполняем команду:
php artisan migrateПосле этого в таблице posts появится колонка deleted_at. Если вы заглянете в структуру таблицы через любой клиент БД, то увидите что-то похожее:
| id | title | body | created_at | updated_at | deleted_at |
|---|---|---|---|---|---|
| 1 | Первая запись | Текст... | 2026-03-02 | 2026-03-04 | NULL |
| 2 | Вторая запись | Текст... | 2026-03-02 | 2026-03-06 | NULL |
Пока все записи активны — колонка deleted_at везде NULL.
Выполняем мягкое удаление
Теперь самое интересное. Для мягкого удаления записи мы вызываем... обычный метод delete(). Да, всё верно — код не меняется.
$post = Post::find(1);
$post->delete();Если мы посмотрим в базу данных после выполнения этого кода, то увидим, что строка с id = 1 никуда не делась — просто в поле deleted_at появилась текущая дата и время:
| id | title | body | created_at | updated_at | deleted_at |
|---|---|---|---|---|---|
| 1 | Первая запись | Текст... | 2026-03-02 | 2026-03-04 | 2026-03-08 09:26:49 |
| 2 | Вторая запись | Текст... | 2026-03-02 | 2026-03-06 | NULL |
Запись помечена как удалённая, но физически остаётся в таблице.
Проверяем статус модели
Иногда в коде нужно узнать, находится ли модель в "корзине" или нет. Для этого есть метод trashed():
$post = Post::find(1);
if ($post->trashed()) {
// Запись мягко удалена
} else {
// Запись активна
}Использовать trashed() лучше, чем проверять is_null($post->deleted_at) вручную — так код чище и понятнее.
На этом базовая настройка завершена. Всего пять шагов, и ваша модель готова к мягкому удалению. В следующем разделе разберём, как работать с мягко удалёнными записями: получать их, восстанавливать и удалять по-настоящему.
Работа с мягко удалёнными моделями
Когда модель настроена на мягкое удаление, появляется несколько важных нюансов в работе с запросами. Нужно ли показывать удалённые записи в списке? Как их восстановить, если пользователь передумал? А может, всё-таки удалить по-настоящему?
Давайте разберём все возможные сценарии.
Обычные запросы: удалённые не беспокоят
Самое приятное в soft delete — что для базовых операций ничего не меняется. Когда делаете обычный запрос через Eloquent, мягко удалённые записи автоматически исключаются из результата:
// Вернёт ТОЛЬКО активные (не удалённые) записи
$activePosts = Post::all();
$recentPosts = Post::where('created_at', '>', now()->subDays(7))->get();
$firstPost = Post::find(1); // если post_id = 1 мягко удалён - вернёт nullЭто работает благодаря глобальному скоупу SoftDeletingScope, который добавляет условие WHERE deleted_at IS NULL к каждому запросу. Вы об этом даже не думаете — фреймворк всё делает сам.
Получаем все записи, включая удалённые
Но что, если нужен полный список, включая «почищенные» записи? Например, для панели управления, где мы хотим видеть, что вообще есть в системе. На помощь приходит метод withTrashed():
// Вернёт ВСЕ записи - и активные, и мягко удалённые
$allPosts = Post::withTrashed()->get();
// Можно комбинировать с другими условиями
$recentPosts = Post::withTrashed()
->where('created_at', '>', now()->subDays(30))
->get();
// Найти конкретную запись, даже если она удалена
$post = Post::withTrashed()->find(1);Ищем только среди удалённых
Бывает и обратная задача — показать только то, что лежит в «корзине». Например, на странице восстановления удалённых записей. Для этого есть метод onlyTrashed():
// Вернёт только мягко удалённые записи
$trashedPosts = Post::onlyTrashed()->get();
// Можно уточнять запрос
$oldTrashedPosts = Post::onlyTrashed()
->where('deleted_at', '<', now()->subMonth())
->get();Восстанавливаем удалённое
Если пользователь передумал или запись была помечена как удалённая по ошибке, её легко вернуть к жизни методом restore():
$post = Post::withTrashed()->find(1);
$post->restore();После выполнения этого кода поле deleted_at снова станет NULL, и запись снова будет появляться в обычных запросах.
Удаляем по-настоящему (forceDelete)
Иногда мягкое удаление — лишь первый шаг, а потом запись нужно удалить окончательно. Например, пользователь запросил полное удаление своих данных. Для этого используйте forceDelete():
$post = Post::withTrashed()->find(1);
$post->forceDelete(); // запись исчезнет из базы навсегдаБудьте осторожны с этим методом — восстановить данные после него уже не получится.
Проверяем статус модели
В коде часто нужно узнать, находится ли конкретная модель в «корзине» или нет. Метод trashed() возвращает true, если запись мягко удалена:
$post = Post::withTrashed()->find(1);
if ($post->trashed()) {
echo "Запись в корзине. Время удаления: " . $post->deleted_at;
} else {
echo "Запись активна";
}Предупреждение про фасад DB
Всё, что мы обсуждали выше, работает только внутри Eloquent. Если вы пишете запросы через фасад DB, глобальный скоуп не применяется. И тут легко ошибиться:
// Eloquent - безопасно
$posts = Post::where('category_id', 5)->get();
// DB фасад - вернёт ВСЁ, включая удалённое
$posts = DB::table('posts')->where('category_id', 5)->get();
// Чтобы исключить удалённые, нужно добавлять условие вручную
$posts = DB::table('posts')
->where('category_id', 5)
->whereNull('deleted_at')
->get();Это не проблема самого подхода, а просто особенность, о которой нужно помнить. В сложных отчётах или при оптимизации производительности иногда приходится использовать DB фасад — просто не забывайте про whereNull('deleted_at').
Шпаргалка: Методы soft delete
| Задача | Метод |
|---|---|
| Получить только активные | Post::all() |
| Получить включая удалённые | Post::withTrashed()->get() |
| Получить только удалённые | Post::onlyTrashed()->get() |
| Найти конкретную (даже удалённую) | Post::withTrashed()->find($id) |
| Мягко удалить | $post->delete() |
| Восстановить | $post->restore() |
| Удалить навсегда | $post->forceDelete() |
| Проверить статус | $post->trashed() |
В следующем разделе разберём более сложный, но важный сценарий: как работает привязка модели к маршруту с мягко удалёнными записями.
Soft Delete и привязка к маршруту
В Laravel есть удобная функция — автоматическая подгрузка модели по параметру маршрута. Вместо того чтобы вручную искать запись по ID в контроллере, мы можем сразу указать тайп-хинт модели, и Laravel сам найдёт её и передаст в метод.
// В файле routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);
// В контроллере
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}Если пользователь перейдёт по адресу /posts/123, Laravel автоматически выполнит Post::findOrFail(123) и передаст найденную модель в метод show. Красиво и удобно.
Но есть нюанс с мягким удалением.
Что происходит по умолчанию
По умолчанию Laravel не включает мягко удалённые модели в привязку к маршруту. То есть если запись с ID = 123 была мягко удалена, при переходе по адресу /posts/123 пользователь получит 404 ошибку, даже если запись физически существует в базе.
Для большинства случаев это правильно: обычные посетители не должны видеть удалённый контент. Но в административном интерфейсе нам часто нужно работать и с удалёнными записями — просматривать их, восстанавливать, удалять окончательно.
Разрешаем мягко удалённые модели в маршруте
Чтобы разрешить подгрузку мягко удалённых моделей для конкретного маршрута, используйте метод withTrashed() при определении маршрута:
Route::get('/posts/{post}', [PostController::class, 'show'])
->withTrashed(); // теперь найдёт и удалённую записьТеперь, если пользователь (например, администратор) перейдёт по адресу /posts/123, он увидит запись, даже если она в корзине.
Выборочное применение
Иногда нужно разрешить мягко удалённые модели не для всех методов ресурсного контроллера, а только для некоторых. Например, мы хотим показывать удалённые записи (show), редактировать их (edit, update), но не возвращать в списках (index). Laravel позволяет передать в withTrashed() массив методов, к которым это должно применяться:
Route::resource('posts', PostController::class)
->withTrashed(['show', 'edit', 'update']);Теперь для маршрутов posts.show, posts.edit и posts.update мягко удалённые модели будут подгружаться, а для остальных — нет.
Групповая регистрация ресурсов
В Laravel 12.x появился удобный метод для регистрации нескольких ресурсных контроллеров сразу с применением withTrashed() ко всем:
Route::softDeletableResources([
'posts' => PostController::class,
'comments' => CommentController::class,
'users' => UserController::class,
]);Это эквивалентно тому, что для каждого из этих ресурсов мы вызвали бы ->withTrashed().
Проверяем статус в контроллере
Даже если мы разрешили мягко удалённые модели в маршруте, иногда нужно дополнительно проверить, является ли текущая запись удалённой. Например, чтобы показать специальное предупреждение на странице:
public function show(Post $post)
{
if ($post->trashed()) {
// Можно передать во вью флаг, что запись в корзине
return view('posts.show', [
'post' => $post,
'isTrashed' => true
]);
}
return view('posts.show', ['post' => $post]);
}Шпаргалка: Soft delete и привязка к маршрутам
| Ситуация | Что делать |
|---|---|
| Обычные маршруты (для всех пользователей) | Ничего не менять — 404 для удалённых записей |
| Маршруты в панели управления, где нужны удалённые | Добавить ->withTrashed() при определении маршрута |
| Нужно только для некоторых методов | ->withTrashed(['show', 'edit']) |
| Много ресурсных контроллеров сразу | Route::softDeletableResources([...]) |
| В контроллере нужно узнать статус | $model->trashed() |
В следующем разделе перейдём к тестированию — как убедиться, что всё работает правильно, и не пропустить случайно мягко удалённые записи там, где их быть не должно.
Тестирование Soft Delete
Любая функциональность в приложении должна покрываться тестами, и мягкое удаление — не исключение. Нам нужно проверять, что записи действительно помечаются как удалённые, что они не попадают в обычные выборки, и что восстановление работает корректно. Laravel предоставляет для этого удобные встроенные методы.
Простой тест
Начнём с базовой проверки: после вызова метода delete() запись должна стать мягко удалённой.
use App\Models\Post;
use PHPUnit\Framework\Attributes\Test;
public function test_post_can_be_soft_deleted(): void
{
$post = Post::factory()->create();
$post->delete();
// Проверяем, что запись стала мягко удалённой
$this->assertSoftDeleted($post);
}Метод assertSoftDeleted() проверяет, что у модели заполнено поле deleted_at. Есть и обратный метод — assertNotSoftDeleted().
Тест для маршрута
Чаще нам нужно тестировать не прямой вызов метода, а работу всего маршрута и контроллера. Допустим, есть эндпоинт для удаления поста:
public function test_post_can_be_soft_deleted_via_api(): void
{
$post = Post::factory()->create();
$response = $this->deleteJson("/api/posts/{$post->id}");
$response->assertNoContent(); // 204 - успешное удаление без контента
$this->assertSoftDeleted($post);
}Создаём уже удалённые записи для тестов
Часто в тестах нужно иметь дело с уже мягко удалёнными записями — например, чтобы проверить, что они не показываются в списках, или что восстановление работает. В фабриках Laravel для этого есть встроенный метод trashed():
// Создаём уже мягко удалённую запись
$trashedPost = Post::factory()->trashed()->create();
// Можно комбинировать с другими состояниями
$oldTrashedPost = Post::factory()
->trashed()
->create(['created_at' => now()->subMonth()]);Проверяем, что удалённые записи не попадают в выборку
Одна из ключевых гарантий soft delete — что в обычных запросах удалённые записи не возвращаются. Это тоже стоит протестировать:
public function test_soft_deleted_posts_are_excluded_from_normal_queries(): void
{
// Создаём активный пост
$activePost = Post::factory()->create();
// Создаём мягко удалённый пост
$trashedPost = Post::factory()->trashed()->create();
// Обычный запрос должен вернуть только активный
$posts = Post::all();
$this->assertTrue($posts->contains($activePost));
$this->assertFalse($posts->contains($trashedPost));
}Тестируем withTrashed и onlyTrashed
Аналогично проверяем, что специальные методы работают как ожидается:
public function test_with_trashed_includes_deleted_posts(): void
{
$activePost = Post::factory()->create();
$trashedPost = Post::factory()->trashed()->create();
$posts = Post::withTrashed()->get();
$this->assertTrue($posts->contains($activePost));
$this->assertTrue($posts->contains($trashedPost));
}
public function test_only_trashed_returns_only_deleted_posts(): void
{
$activePost = Post::factory()->create();
$trashedPost = Post::factory()->trashed()->create();
$trashedPosts = Post::onlyTrashed()->get();
$this->assertFalse($trashedPosts->contains($activePost));
$this->assertTrue($trashedPosts->contains($trashedPost));
}Тестируем восстановление
И конечно, проверяем, что метод restore() возвращает запись к жизни:
public function test_soft_deleted_post_can_be_restored(): void
{
$post = Post::factory()->trashed()->create();
$this->assertSoftDeleted($post);
$post->restore();
$this->assertNotSoftDeleted($post);
}Для маршрута восстановления (например, POST /api/posts/{id}/restore) тест будет выглядеть аналогично первому примеру.
Проверяем полное удаление
Когда используем forceDelete(), запись должна исчезнуть из базы полностью. Для проверки используйте assertModelMissing():
public function test_post_can_be_permanently_deleted(): void
{
$post = Post::factory()->trashed()->create();
$post->forceDelete();
$this->assertModelMissing($post);
}Шпаргалка: Тестирование soft delete
| Что проверяем | Метод |
|---|---|
| Модель мягко удалена | assertSoftDeleted($model) |
| Модель не удалена | assertNotSoftDeleted($model) |
| Модель удалена навсегда | assertModelMissing($model) |
| Модель существует в БД | assertModelExists($model) |
| Создать уже удалённую модель | Model::factory()->trashed()->create() |
В следующем разделе поговорим о том, как не дать базе данных разрастаться бесконечно — про автоматическую очистку старых мягко удалённых записей.
Очистка старых записей с помощью Prunable
Мы уже говорили о недостатках soft delete — в частности, о том, что база данных со временем разрастается. Удалённые записи продолжают храниться и занимать место. Хорошая новость: Laravel предоставляет элегантный механизм для автоматической очистки старых мягко удалённых записей. Называется он Prunable.
Это трейт, который можно добавить в модель, и метод prunable(), в котором мы определяем, какие записи считаются "устаревшими" и подлежат окончательному удалению. Laravel сам будет находить такие записи и безвозвратно их стирать.
Настраиваем Prunable в модели
Добавим трейт Prunable в модель Post и определим метод prunable():
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Post extends Model
{
use HasFactory, SoftDeletes, Prunable;
/**
* Get the prunable model query.
*/
public function prunable()
{
return static::where('deleted_at', '<=', now()->subMonth());
}
}
В этом примере мы указали, что через месяц после мягкого удаления записи становятся кандидатами на окончательное удаление.
Можно задать и более сложные условия. Например, удалять через месяц, но только те записи, которые не были восстановлены и не имеют связанных комментариев:
public function prunable()
{
return static::where('deleted_at', '<=', now()->subMonth())
->whereDoesntHave('comments');
}Добавляем планировщик
Трейт Prunable только определяет, какие записи нужно удалять. Но сам процесс нужно запускать. Для этого в Laravel есть Artisan-команда model:prune.
Добавим её в планировщик задач в файле app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('model:prune')->daily();
}Теперь каждый день Laravel будет проходить по всем моделям, использующим трейт Prunable, и удалять записи, подходящие под условия в методе prunable().
Тонкая настройка: массовое удаление
По умолчанию model:prune удаляет записи пачками. Вы можете настроить размер пачки, добавив свойство $prunable в модель:
/**
* The number of models to return per pruning batch.
*
* @var int
*/
protected $prunable = 100;Исключаем модели из очистки
Если нужно, чтобы некоторые модели с трейтом Prunable не обрабатывались командой (например, в тестах или на отдельных окружениях), можно использовать опцию --except:
php artisan model:prune --except=App\\Models\\PostИли исключить несколько моделей сразу:
php artisan model:prune --except="App\\Models\\Post,App\\Models\\Comment"Выполняем дополнительные действия перед удалением
Иногда перед окончательным удалением нужно сделать что-то ещё: записать в лог, отправить уведомление, отсоединить связанные файлы. Для этого в трейте Prunable есть метод pruning(), который вызывается непосредственно перед удалением каждой модели:
public function pruning()
{
// Удаляем связанное изображение
Storage::delete($this->image_path);
// Выводим в лог событие
Log::info('Post permanently deleted', ['id' => $this->id]);
}Шпаргалка: Очистка удалённых старых записей
| Что нужно сделать | Как это сделать |
|---|---|
| Добавить очистку в модель | use Prunable и определить prunable() |
| Указать, что удалять | return static::where('deleted_at', '<=', now()->subMonth()); |
| Запускать автоматически | Добавить $schedule->command('model:prune')->daily(); в Kernel.php |
| Выполнить дополнительные действия | Определить метод pruning() в модели |
Мы разобрали практически всё, что нужно знать о soft delete в Laravel. Осталось подвести итог.
Заключение
Мы проделали большой путь — от самого понятия мягкого удаления до продвинутых техник работы с ним. Давайте кратко резюмируем главное.
Soft delete — это инструмент, который даёт страховочную сетку при удалении данных. Вместо того чтобы безвозвратно стирать строки из базы, мы просто помечаем их как «удалённые» с помощью временной метки в колонке deleted_at. Это позволяет восстанавливать ошибочно удалённые записи, хранить данные для аналитики и выстраивать многоступенчатые процессы удаления.
Laravel делает работу с мягким удалением на редкость удобной. Добавили трейт SoftDeletes в модель, создали миграцию с методом softDeletes() — и вся магия готова. Дальше можно пользоваться знакомыми методами delete(), restore(), forceDelete() и специальными запросами withTrashed() и onlyTrashed().
Но, как у любого инструмента, у soft delete есть и обратная сторона:
- Рост базы данных — удалённые записи продолжают храниться и занимать место.
- Риск случайно включить «удалённые» данные в запросы, особенно при использовании фасада DB.
- Вопросы соответствия GDPR — просто поставить флаг
deleted_atнедостаточно для выполнения требований по удалению персональных данных.
Хорошая новость в том, что Laravel даёт средства для решения этих проблем. Prunable помогает автоматически очищать старые записи, а встроенные методы тестирования позволяют убедиться, что мягко удалённые данные ведут себя правильно.
Не делайте soft delete привычкой по умолчанию для всех моделей. Это осознанный выбор, у которого есть цена. Но когда мягкое удаление действительно нужно, Laravel предоставляет все инструменты, чтобы реализовать его чисто, безопасно и с минимальными усилиями.
Надеюсь, это руководство помогло вам разобраться в теме. Если остались вопросы — загляните в официальную документацию или перечитайте нужный раздел. Счастливого кодинга!