├── .phpunit.cache └── test-results ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── queued-db-cleanup.php └── src ├── CleanConfig.php ├── CleanDatabaseJobFactory.php ├── Events ├── CleanDatabaseCompleted.php ├── CleanDatabasePassCompleted.php └── CleanDatabasePassStarting.php ├── Exceptions ├── CouldNotCreateJob.php └── InvalidDatabaseCleanupJobClass.php ├── Jobs └── CleanDatabaseJob.php └── LaravelQueuedDbCleanupServiceProvider.php /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#0":8,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#1":8,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#2":8,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#3":8},"times":{"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#0":0.081,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#1":0.017,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#2":0.016,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_in_the_right_amount_of_passes#3":0.024,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_continue_deleting_until_a_specified_condition":0.011,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_dispatches_a_start_event":0.002,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_accepts_database_query_builder":0.002,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_will_not_clean_if_it_cannot_get_the_lock":0.005,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::the_job_can_be_serialized":0.001,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_respects_the_bindings":0.004,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_use_a_custom_database_cleanup_job_class":0.001,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_throws_an_exception_if_an_invalid_job_class_is_used":0.001,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_throws_an_exception_if_no_query_was_set":0,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_throws_an_exception_if_no_chunk_size_was_set":0,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_use_a_custom_connection":0.001,"Spatie\\LaravelQueuedDbCleanup\\Tests\\CleansUpDatabaseTest::it_can_delete_records_on_custom_connection":0.005}} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-queued-db-cleanup` will be documented in this file 4 | 5 | ## 1.5.1 - 2025-02-21 6 | 7 | ### What's Changed 8 | 9 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-queued-db-cleanup/pull/22 10 | 11 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.5.0...1.5.1 12 | 13 | ## 1.5.0 - 2024-09-23 14 | 15 | ### What's Changed 16 | 17 | * Allow doctrine/dbal 4.0 by @mbardelmeijer in https://github.com/spatie/laravel-queued-db-cleanup/pull/21 18 | 19 | ### New Contributors 20 | 21 | * @mbardelmeijer made their first contribution in https://github.com/spatie/laravel-queued-db-cleanup/pull/21 22 | 23 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.4.0...1.5.0 24 | 25 | ## 1.4.0 - 2024-03-08 26 | 27 | ### What's Changed 28 | 29 | * Laravel 11.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-queued-db-cleanup/pull/19 30 | 31 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.3.3...1.4.0 32 | 33 | ## 1.3.3 - 2023-07-18 34 | 35 | ### What's Changed 36 | 37 | - Fix limit not being applied to query by @dmason30 in https://github.com/spatie/laravel-queued-db-cleanup/pull/18 38 | 39 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.3.2...1.3.3 40 | 41 | ## 1.3.2 - 2023-06-28 42 | 43 | ### What's Changed 44 | 45 | - Avoid serializing `CleanConfig` object inside `stopWhen` parameter by @Plytas in https://github.com/spatie/laravel-queued-db-cleanup/pull/16 46 | 47 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.3.1...1.3.2 48 | 49 | ## 1.3.1 - 2023-06-28 50 | 51 | ### What's Changed 52 | 53 | - Support for database query builder by @Plytas in https://github.com/spatie/laravel-queued-db-cleanup/pull/17 54 | 55 | ### New Contributors 56 | 57 | - @Plytas made their first contribution in https://github.com/spatie/laravel-queued-db-cleanup/pull/17 58 | 59 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.3.0...1.3.1 60 | 61 | ## 1.3.0 - 2023-04-07 62 | 63 | - improve handling deadlocks 64 | 65 | ## 1.2.1 - 2023-01-24 66 | 67 | ### What's Changed 68 | 69 | - Laravel 10.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-queued-db-cleanup/pull/14 70 | 71 | ### New Contributors 72 | 73 | - @laravel-shift made their first contribution in https://github.com/spatie/laravel-queued-db-cleanup/pull/14 74 | 75 | **Full Changelog**: https://github.com/spatie/laravel-queued-db-cleanup/compare/1.2.0...1.2.1 76 | 77 | ## 1.2.0 - 2022-01-13 78 | 79 | - support Laravel 9 80 | 81 | ## 1.1.3 - 2021-10-04 82 | 83 | - switch to laravel/serializable-closure (#12) 84 | 85 | ## 1.1.2 - 2021-03-09 86 | 87 | - allow v2 and v3 of doctrine for PHP 8 compat (#10) 88 | 89 | ## 1.1.1 - 2021-01-09 90 | 91 | - allow PHP8 (#9) 92 | 93 | ## 1.1.0 - 2020-10-01 94 | 95 | - added support for multiple connections (#7) 96 | 97 | ## 1.0.1 - 2020-09-25 98 | 99 | - fix service provider 100 | 101 | ## 1.0.0 - 2020-09-25 102 | 103 | - initial release 104 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Safely delete large numbers of records 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-queued-db-cleanup.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-queued-db-cleanup) 4 | ![Tests](https://github.com/spatie/laravel-queued-db-cleanup/workflows/Tests/badge.svg) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-queued-db-cleanup.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-queued-db-cleanup) 6 | 7 | Deleting many database records in one go using Laravel has a few pitfalls you need to be aware of: 8 | 9 | - deleting records is possibly a slow operation that can take a long time, 10 | - the delete query will acquire many row locks and possible lock your entire table, other queries will need to wait 11 | - even when managing query execution and cleanup, there's a fixed maximum execution time in a serverless environment 12 | 13 | The pitfalls are described in more detail in [this post](https://flareapp.io/blog/7-how-to-safely-delete-records-in-massive-tables-on-aws-using-laravel) on the [Flare blog](https://flareapp.io/). 14 | 15 | This package offers a solution to safely delete many records in large tables. Here's an example: 16 | 17 | ```php 18 | Spatie\LaravelQueuedDbCleanup\CleanDatabaseJobFactory::new() 19 | ->query(YourModel::query()->where('created_at', '<', now()->subMonth())) 20 | ->deleteChunkSize(1000) 21 | ->dispatch(); 22 | ``` 23 | 24 | The code above will dispatch a cleanup job that will delete the first 1000 records that are selected by the query. When it detects that 1000 records have been deleted, it will conclude that possibly not all records are deleted and it will redispatch itself. 25 | 26 | We'll also make sure that this cleanup job never overlaps. This way the number of database connections is kept low. It also allows you the schedule this cleanup job repeatedly through CRON without having to check for an existing cleanup process. 27 | 28 | By keeping the chunk size small, the query executes faster and potential table locks will not be held for long periods of time. The cleanup job will also finish fast, so you won't hit an execution time limit. 29 | 30 | ## Support us 31 | 32 | [](https://spatie.be/github-ad-click/laravel-queued-db-cleanup) 33 | 34 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 35 | 36 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 37 | 38 | ## Installation 39 | 40 | You can install the package via composer: 41 | 42 | ```bash 43 | composer require spatie/laravel-queued-db-cleanup 44 | ``` 45 | 46 | The package uses a lock to prevent multiple deletions for the same query to be executed at the same time. We recommend using Redis to store the lock. 47 | 48 | Behind the scenes this package leverages [job batches](https://laravel.com/docs/master/queues#job-batching). Make sure you have created the batches table mentioned in the Laravel documentation. 49 | 50 | Optionally, you can publish the config file with: 51 | 52 | ```bash 53 | php artisan vendor:publish --provider="Spatie\LaravelQueuedDbCleanup\LaravelQueuedDbCleanupServiceProvider" --tag="config" 54 | ``` 55 | 56 | This is the contents of the published config file: 57 | 58 | ```php 59 | return [ 60 | /* 61 | * To make sure there's only one job of a particular cleanup running, 62 | * this package uses a lock. Here, you can configure the default 63 | * store to be used by the lock and the release time. 64 | */ 65 | 'lock' => [ 66 | 'cache_store' => 'redis', 67 | 68 | 'release_lock_after_seconds' => 60 * 20 69 | ], 70 | 71 | /* 72 | * The class name of the job that will clean that database. 73 | * 74 | * This should be `Spatie\LaravelQueuedDbCleanup\Jobs\CleanDatabaseJob` 75 | * or a class that extends it. 76 | */ 77 | 'clean_database_job_class' => Spatie\LaravelQueuedDbCleanup\Jobs\CleanDatabaseJob::class, 78 | 79 | /* 80 | * In order to handle deadlocks on a high traffic table, the package can 81 | * automatically retry the transaction that performs the delete query 82 | * a specified number of times 83 | */ 84 | 'delete_query_attempts' => 3, 85 | ]; 86 | ``` 87 | 88 | ## Usage 89 | 90 | This code will dispatch a cleanup job that will delete the first 1000 records that are selected by the query. When it detects that 1000 records have been deleted, it will conclude that possibly not all records are deleted and it will redispatch itself. 91 | 92 | ```php 93 | Spatie\LaravelQueuedDbCleanup\CleanDatabaseJobFactory::new() 94 | ->query(YourModel::query()->where('created_at', '<', now()->subMonth())) 95 | ->deleteChunkSize(1000) 96 | ->dispatch(); 97 | ``` 98 | 99 | The job will not redispatch itself when there were fewer records deleted than the number given to `deleteChunkSize`. 100 | 101 | ### Starting the cleanup in a scheduled tasks 102 | 103 | It is safe to start the cleanup process from within a scheduled task. Internally the package will use a lock to make sure no two cleanups using the same query are running at the same time. 104 | 105 | If a scheduled task starts a cleanup process while another one is still running, the new cleanup process will be cancelled. 106 | 107 | ### Customizing the queue and connection name 108 | 109 | Internally, the package uses job batches. Using `getBatch` you can get the batch and call methods like `onConnection` and `onQueue` on it. Don't forget to dispatch the batch at the end, by calling `dispatch()`. 110 | 111 | ```php 112 | Spatie\LaravelQueuedDbCleanup\CleanDatabaseJobFactory::new() 113 | ->query(YourModel::query()->where('created_at', '<', now()->subMonth())) 114 | ->deleteChunkSize(1000) 115 | ->getBatch() 116 | ->onConnection('redis') 117 | ->onQueue('cleanups') 118 | ->dispatch(); 119 | ``` 120 | 121 | ### Customizing the database connection 122 | 123 | Using `onDatabaseConnection` will allow you to delete records on another connection. 124 | 125 | ```php 126 | Spatie\LaravelQueuedDbCleanup\CleanDatabaseJobFactory::new() 127 | ->query(YourModel::query()) 128 | ->onDatabaseConnection('other_connection') 129 | ->deleteChunkSize(1000) 130 | ->dispatch(); 131 | ``` 132 | ### Manually stopping the cleanup process 133 | 134 | By default, the cleanup jobs will not redispatch themselves anymore when they detect that they've deleted fewer records than the chunk size. You can customize this behaviour by calling `stopWhen`. It should receive a closure. If the closure returns `true` the cleanup will stop. 135 | 136 | ```php 137 | CleanDatabaseJobFactory::forQuery(YourModel::query()) 138 | ->deleteChunkSize(10) 139 | ->stopWhen(function (Spatie\LaravelQueuedDbCleanup\CleanConfig $config) { 140 | return $config->pass === 3; 141 | }) 142 | ->dispatch(); 143 | ``` 144 | 145 | `stopWhen` receives an instance of `Spatie\LaravelQueuedDbCleanup\CleanConfig`. It contains these properties to determine whether the cleanup should be stopped: 146 | 147 | - `pass`: contains the number of times the cleanup job was started for this particular cleanup. 148 | - `rowsDeletedInThisPass`: the number of rows deleted in this pass 149 | - `totalRowsDeleted`: the total of number of rows deleted by in all passes. 150 | 151 | ### Using the batch to stop the cleanup process 152 | 153 | You can use the batch id to stop the cleanup process 154 | 155 | ```php 156 | $batch = CleanDatabaseJobFactory::forQuery(YourModel::query()) 157 | ->deleteChunkSize(10) 158 | ->getBatch(); 159 | 160 | // you could store this batch id somewhere 161 | $batchId = $batch->id; 162 | 163 | $batch->dispatch(); 164 | ``` 165 | 166 | Somewhere else in your codebase you could retrieve the stored batch id and use it to cancel the batch, stopping the cleanup process. 167 | 168 | ```php 169 | \Illuminate\Support\Facades\Bus::findBatch($batchId)->cancel(); 170 | ``` 171 | 172 | ## Events 173 | 174 | You can listen for these events. They all have one public property, `cleanConfig`, which is an instance of `Spatie\LaravelQueuedDbCleanup\CleanConfig`. 175 | 176 | ### Spatie\LaravelQueuedDbCleanup\Events\CleanDatabasePassStarting 177 | 178 | Fired when a new pass starts in the cleanup process. 179 | 180 | ### Spatie\LaravelQueuedDbCleanup\Events\CleanDatabasePassCompleted 181 | 182 | Fired when a pass has been completed in the cleanup process. 183 | 184 | ### Spatie\LaravelQueuedDbCleanup\Events\CleanDatabaseCompleted 185 | 186 | Fired when the entire cleanup process has been completed. 187 | 188 | ## Testing 189 | 190 | ``` bash 191 | composer test 192 | ``` 193 | 194 | ## Changelog 195 | 196 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 197 | 198 | ## Contributing 199 | 200 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 201 | 202 | ## Security Vulnerabilities 203 | 204 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 205 | 206 | ## Credits 207 | 208 | - [Freek Van der Herten](https://github.com/freekmurze) 209 | - [All Contributors](../../contributors) 210 | 211 | ## License 212 | 213 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 214 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-queued-db-cleanup", 3 | "description": "Safely delete large numbers of records", 4 | "keywords": [ 5 | "spatie", 6 | "laravel-queued-db-cleanup", 7 | "delete", 8 | "eloquent", 9 | "vapor" 10 | ], 11 | "homepage": "https://github.com/spatie/laravel-queued-db-cleanup", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Freek Van der Herten", 16 | "email": "freek@spatie.be", 17 | "homepage": "https://spatie.be", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.2", 23 | "doctrine/dbal": "^3.0|^4.0", 24 | "illuminate/contracts": "^10.0|^11.0|^12.0", 25 | "laravel/serializable-closure": "^1.0|^2.0" 26 | }, 27 | "require-dev": { 28 | "orchestra/testbench": "^8.0|^9.0|^10.0", 29 | "phpunit/phpunit": "^10.5|^11.5.3" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Spatie\\LaravelQueuedDbCleanup\\": "src", 34 | "Spatie\\LaravelQueuedDbCleanup\\Tests\\Database\\Factories\\": "tests/database/factories" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Spatie\\LaravelQueuedDbCleanup\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "vendor/bin/phpunit --colors=always", 44 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 45 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true, 52 | "extra": { 53 | "laravel": { 54 | "providers": [ 55 | "Spatie\\LaravelQueuedDbCleanup\\LaravelQueuedDbCleanupServiceProvider" 56 | ] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/queued-db-cleanup.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'cache_store' => 'redis', 11 | 12 | 'release_lock_after_seconds' => 60 * 20, 13 | ], 14 | 15 | /* 16 | * The class name of the job that will clean that database. 17 | * 18 | * This should be `Spatie\LaravelQueuedDbCleanup\Jobs\CleanDatabaseJob` 19 | * or a class that extends it. 20 | */ 21 | 'clean_database_job_class' => Spatie\LaravelQueuedDbCleanup\Jobs\CleanDatabaseJob::class, 22 | 23 | /* 24 | * In order to handle deadlocks on a high traffic table, the package can 25 | * automatically retry the transaction that performs the delete query 26 | * a specified number of times 27 | */ 28 | 'delete_query_attempts' => 3, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/CleanConfig.php: -------------------------------------------------------------------------------- 1 | lockCacheStore = config('queued-db-cleanup.lock.cache_store'); 42 | 43 | $this->releaseLockAfterSeconds = config('queued-db-cleanup.lock.release_lock_after_seconds'); 44 | } 45 | 46 | /** 47 | * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 48 | */ 49 | public function usingQuery($query, int $chunkSize) 50 | { 51 | $baseQuery = $query instanceof \Illuminate\Database\Eloquent\Builder 52 | ? $query->toBase() 53 | : $query; 54 | 55 | $this->sql = $baseQuery->limit($chunkSize)->getGrammar()->compileDelete($baseQuery); 56 | 57 | $this->sqlBindings = $baseQuery->getBindings(); 58 | 59 | $this->deleteChunkSize = $chunkSize; 60 | 61 | $this->lockName = $this->convertQueryToLockName($baseQuery); 62 | 63 | if ($this->stopWhen === null) { 64 | $this->stopWhen(function (CleanConfig $cleanConfig) { 65 | return $cleanConfig->rowsDeletedInThisPass < $cleanConfig->deleteChunkSize; 66 | }); 67 | } 68 | } 69 | 70 | public function displayName(string $displayName): self 71 | { 72 | $this->displayName = $displayName; 73 | 74 | return $this; 75 | } 76 | 77 | public function tags(array $tags): self 78 | { 79 | $this->tags = $tags; 80 | 81 | return $this; 82 | } 83 | 84 | public function executeDeleteQuery(): int 85 | { 86 | return DB::connection($this->connection)->delete($this->sql, $this->sqlBindings); 87 | } 88 | 89 | public function stopWhen(Closure $closure) 90 | { 91 | $wrapper = new SerializableClosure($closure); 92 | 93 | $this->stopWhen = serialize($wrapper); 94 | } 95 | 96 | public function shouldContinueCleaning(): bool 97 | { 98 | /** @var SerializableClosure $wrapper */ 99 | $wrapper = unserialize($this->stopWhen); 100 | 101 | $stopWhen = $wrapper->getClosure(); 102 | 103 | return ! $stopWhen($this); 104 | } 105 | 106 | public function rowsDeletedInThisPass(int $rowsDeleted): self 107 | { 108 | $this->rowsDeletedInThisPass = $rowsDeleted; 109 | 110 | $this->totalRowsDeleted += $rowsDeleted; 111 | 112 | return $this; 113 | } 114 | 115 | public function incrementPass(): self 116 | { 117 | $this->pass++; 118 | 119 | $this->rowsDeletedInThisPass = 0; 120 | 121 | return $this; 122 | } 123 | 124 | public function lock(): Lock 125 | { 126 | return Cache::store($this->lockCacheStore)->lock($this->lockName, $this->releaseLockAfterSeconds); 127 | } 128 | 129 | /** @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ 130 | protected function convertQueryToLockName($query): string 131 | { 132 | return md5($query->toSql().print_r($query->getBindings(), true)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/CleanDatabaseJobFactory.php: -------------------------------------------------------------------------------- 1 | jobClass = config('queued-db-cleanup.clean_database_job_class'); 38 | 39 | $this->cleanConfig = new CleanConfig; 40 | 41 | $this->query = $query; 42 | } 43 | 44 | /** @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query */ 45 | public function query($query): self 46 | { 47 | $this->query = $query; 48 | 49 | return $this; 50 | } 51 | 52 | public function deleteChunkSize(int $size): self 53 | { 54 | $this->deleteChunkSize = $size; 55 | 56 | return $this; 57 | } 58 | 59 | public function jobClass(string $databaseCleanupJobClass): self 60 | { 61 | if (! $this->isValidDatabaseCleanupJobClass($databaseCleanupJobClass)) { 62 | throw InvalidDatabaseCleanupJobClass::make($databaseCleanupJobClass); 63 | } 64 | 65 | $this->jobClass = $databaseCleanupJobClass; 66 | 67 | return $this; 68 | } 69 | 70 | public function displayName(string $displayName): self 71 | { 72 | $this->cleanConfig->displayName = $displayName; 73 | 74 | return $this; 75 | } 76 | 77 | public function tags(array $tags): self 78 | { 79 | $this->cleanConfig->tags = $tags; 80 | 81 | return $this; 82 | } 83 | 84 | public function onDatabaseConnection(string $connection): self 85 | { 86 | $this->cleanConfig->connection = $connection; 87 | 88 | return $this; 89 | } 90 | 91 | public function getJob(): CleanDatabaseJob 92 | { 93 | $this->ensureValid(); 94 | 95 | $this->cleanConfig->usingQuery($this->query, $this->deleteChunkSize); 96 | 97 | return new $this->jobClass($this->cleanConfig); 98 | } 99 | 100 | public function getBatch(): PendingBatch 101 | { 102 | return Bus::batch([$this->getJob()]); 103 | } 104 | 105 | public function dispatch(): Batch 106 | { 107 | return $this->getBatch()->dispatch(); 108 | } 109 | 110 | public function stopWhen(Closure $closure): self 111 | { 112 | $this->cleanConfig->stopWhen($closure); 113 | 114 | return $this; 115 | } 116 | 117 | protected function ensureValid(): void 118 | { 119 | if (is_null($this->query)) { 120 | throw CouldNotCreateJob::queryNotSet(); 121 | } 122 | 123 | if (is_null($this->deleteChunkSize)) { 124 | throw CouldNotCreateJob::deleteChunkSizeNotSet(); 125 | } 126 | } 127 | 128 | protected function isValidDatabaseCleanupJobClass(string $jobClass): bool 129 | { 130 | if ($jobClass === CleanDatabaseJob::class) { 131 | return true; 132 | } 133 | 134 | return is_subclass_of($jobClass, CleanDatabaseJob::class); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Events/CleanDatabaseCompleted.php: -------------------------------------------------------------------------------- 1 | cleanConfig = $cleanConfig; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Events/CleanDatabasePassCompleted.php: -------------------------------------------------------------------------------- 1 | cleanConfig = $cleanConfig; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Events/CleanDatabasePassStarting.php: -------------------------------------------------------------------------------- 1 | cleanConfig = $cleanConfig; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotCreateJob.php: -------------------------------------------------------------------------------- 1 | config = $config; 30 | } 31 | 32 | public function handle() 33 | { 34 | if ($this->batch()->cancelled()) { 35 | return; 36 | } 37 | 38 | if (! $this->config->lock()->get()) { 39 | return; 40 | } 41 | 42 | $numberOfRowsDeleted = $this->performCleaning(); 43 | 44 | $this->config->lock()->forceRelease(); 45 | 46 | $this->config->rowsDeletedInThisPass($numberOfRowsDeleted); 47 | 48 | $this->config->shouldContinueCleaning() 49 | ? $this->continueCleaning() 50 | : $this->finishCleanup(); 51 | } 52 | 53 | public function tags() 54 | { 55 | return $this->config->tags; 56 | } 57 | 58 | public function displayName() 59 | { 60 | return $this->config->displayName ?? static::class; 61 | } 62 | 63 | protected function performCleaning(): int 64 | { 65 | event(new CleanDatabasePassStarting($this->config)); 66 | 67 | return DB::transaction(function () { 68 | return $this->config->executeDeleteQuery(); 69 | }, config('queued-db-cleanup.delete_query_attempts')); 70 | } 71 | 72 | protected function continueCleaning(): void 73 | { 74 | event(new CleanDatabasePassCompleted($this->config)); 75 | 76 | $this->config->incrementPass(); 77 | 78 | $this->batch()->add([new CleanDatabaseJob($this->config)]); 79 | } 80 | 81 | protected function finishCleanup(): void 82 | { 83 | event(new CleanDatabasePassCompleted($this->config)); 84 | 85 | event(new CleanDatabaseCompleted($this->config)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/LaravelQueuedDbCleanupServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 12 | $this->publishes([ 13 | __DIR__.'/../config/queued-db-cleanup.php' => config_path('queued-db-cleanup.php'), 14 | ], 'config'); 15 | } 16 | } 17 | 18 | public function register() 19 | { 20 | $this->mergeConfigFrom(__DIR__.'/../config/queued-db-cleanup.php', 'queued-db-cleanup'); 21 | } 22 | } 23 | --------------------------------------------------------------------------------