spatie/laravel-queued-db-cleanup
Safely delete large numbers of Laravel database records by dispatching non-overlapping queued cleanup jobs. Deletes in small chunks to reduce locks and avoid timeouts, automatically re-dispatching until the query’s records are gone.
Installation:
composer require spatie/laravel-queued-db-cleanup
Publish the config (optional):
php artisan vendor:publish --provider="Spatie\LaravelQueuedDbCleanup\LaravelQueuedDbCleanupServiceProvider"
First Use Case:
Delete old records from a table (e.g., User model) without locking the table:
use Spatie\LaravelQueuedDbCleanup\CleanDatabaseJobFactory;
CleanDatabaseJobFactory::new()
->query(User::query()->where('deleted_at', '<', now()->subDays(30)))
->deleteChunkSize(500) // Adjust based on table size
->dispatch();
Key Files:
config/queued-db-cleanup.php (for customization)app/Jobs/CleanDatabaseJob.php (extendable base job)Job Dispatch:
Use CleanDatabaseJobFactory to create and dispatch a job with a query and chunk size.
CleanDatabaseJobFactory::new()
->query(Post::query()->where('status', 'draft')->where('updated_at', '<', now()->subYear()))
->deleteChunkSize(1000)
->dispatch();
Chunked Deletion:
The job processes records in chunks (default: 100) to avoid locks. Override deleteChunkSize() for larger/smaller batches.
Queue Integration:
database, redis, etc.).php artisan queue:work or Horizon.Soft Deletes:
For models using SoftDeletes, the job handles forceDelete() by default. Override if needed:
->deleteChunkSize(500)
->forceDelete(true) // Explicitly force-delete
Custom Jobs:
Extend CleanDatabaseJob to add pre/post-deletion logic:
namespace App\Jobs;
use Spatie\LaravelQueuedDbCleanup\CleanDatabaseJob;
class CustomCleanupJob extends CleanDatabaseJob {
public function handle() {
// Pre-deletion logic (e.g., log start time)
parent::handle();
// Post-deletion logic (e.g., notify users)
}
}
Dynamic Query Building: Pass a closure to build the query dynamically:
->query(fn () => Model::query()->where('active', false)->where('trial_ends', '<', now()))
Batch Processing: For very large tables, split into multiple jobs:
$query = User::query()->where('created_at', '<', now()->subYears(2));
$total = $query->count();
$batchSize = 5000;
for ($i = 0; $i < $total; $i += $batchSize) {
CleanDatabaseJobFactory::new()
->query($query->offset($i)->limit($batchSize))
->dispatch();
}
Event Listeners:
Trigger cleanup jobs via model events (e.g., deleted):
// In EventServiceProvider
Model::deleted(fn () => CleanDatabaseJobFactory::new()->query(Model::query()->where('id', '<', 100))->dispatch());
Locking:
deleteChunkSize values may cause locks. Start with 100–500 and adjust.Foreign Keys:
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
// Run cleanup
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
Memory Limits:
deleteChunkSize or optimize queries.Queue Stuck Jobs:
php artisan queue:failed to retry or delete.Soft Deletes:
SoftDeletes, ensure the query filters for deleted_at or explicitly call forceDelete() to avoid partial deletions.Log Queries:
Enable Laravel’s query logging in config/logging.php to inspect generated SQL:
'default' => env('LOG_CHANNEL', 'stack'),
'stack' => [
'channels' => ['single'],
'ignore_exceptions' => false,
],
Job Progress:
Add logging to track progress in CleanDatabaseJob:
public function handle() {
Log::info("Deleting batch {$this->batchNumber} of {$this->totalBatches}");
// ...
}
Test Locally:
queue:work --once to test jobs locally before deploying to production.Model::factory()->count(10000)->create().Queue Connection:
Defaults to default queue. Override in config/queued-db-cleanup.php:
'queue_connection' => 'redis',
Batch Size:
The deleteChunkSize is not the same as Laravel’s chunk() size. It controls the number of records deleted per database transaction.
Model Binding: Ensure the query returns the correct model. For polymorphic relations, use explicit casts:
->query(Post::withTrashed()->where('user_id', auth()->id()))
Custom Cleanup Logic:
Override CleanDatabaseJob::deleteModel() to add logic (e.g., archive before deletion):
protected function deleteModel($model) {
$model->archive();
$model->delete();
}
Pre/Post Hooks: Use Laravel’s job middleware to add hooks:
// In AppServiceProvider
$this->app->booted(fn () => CleanDatabaseJob::addMiddleware(\App\Jobs\LogCleanupMiddleware::class));
Retry Logic: Implement custom retry logic for failed batches:
public function failed(\Throwable $exception) {
Log::error("Cleanup failed: " . $exception->getMessage());
// Notify admin or retry with a smaller chunk
}
How can I help you explore Laravel packages today?