Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Adds the possibility of having "Prunable" models #37889

Merged
merged 10 commits into from
Jul 6, 2021

Conversation

nunomaduro
Copy link
Member

@nunomaduro nunomaduro commented Jul 2, 2021

This pull request adds a convenient way to remove obsolete model records by adding the possibility of making Laravel models prunable. When making models prunable, Laravel will automatically remove obsolete model records from the database via a scheduled command.

Usage

To get started, add the Illuminate\Database\Eloquent\Prunable or Illuminate\Database\Eloquent\MassPrunable trait to the model you would like to make prunable.

Prunable vs MassPrunable: When making models mass prunable, the delete, and deleting model events will not be fired for the deleted models. This is because the models are never actually retrieved. Making the entire pruning process much faster.

Next, implement a prunable method to determine the prunable query:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable; // or MassPrunable

class Post extends Model
{
    use Prunable; // or MassPrunable

    /**
     * Determines the prunable query.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function prunable()
    {
        return $this->where('created_at', '<=', now()->subMonth());
    }
}

Finally, schedule the db:prune artisan command in your App\Console\Kernel class:

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('db:prune')->daily();
    }

Behind the scenes, the db:prune artisan command will automatically detect Prunable models in the app/models folder. Yet, if your models are in a different location, you may use the --model artisan option to specify the class names:

        $schedule->command('db:prune', [
            '--model' => User::class, // [User::class, Post::class]
        ])->daily();

Soft Deleted Models

If your Eloquent model is Prunable or MassPrunable, and uses SoftDeletes, all soft deleted models are automatically be included on the prunable query. Meaning that if you return a prunable query on the prunable() method that deleted Posts that are 30 days old, soft deleted posts that are 30 days old will be automatically deleted as well. This behavior, regarding automatically deleting soft deleted models, can not (and should not) be modified.

Configuring Prune Method

When using the Illuminate\Database\Eloquent\Prunable trait, a prune() method will be called on each model. By default, this method will call the $this->delete() or the $this->forceDelete() - when using the SoftDeletes trait - methods. If you would like to customize the prune method, you may override it on the model itself:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;

class Post extends Model
{
    use Prunable;

    /**
     * Determines the prunable query.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function prunable()
    {
        return $this->where('created_at', '<=', now()->subMonth());
    }

    /**
     * Prune the model in the database.
     *
     * @return bool|null
     */
    public function prune()
    {
        // Delete resources associated with the Post model...

        return $this->delete();
    }
}

Open questions

  1. Should we allow to customize the chunk size? If yes, at command level (applied at all models), or at model level?
  2. Should the command be called db:prune or model:prune?

Copy link
Contributor

@brunogaspar brunogaspar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nunomaduro this looks fantastic and will clean some codebases out there (mine included)!

Just a few suggestions on wording and with some typos also fixed (that i could find at least).

src/Illuminate/Database/Console/PruneCommand.php Outdated Show resolved Hide resolved
src/Illuminate/Database/Console/PruneCommand.php Outdated Show resolved Hide resolved
src/Illuminate/Database/Events/ModelsPruned.php Outdated Show resolved Hide resolved
src/Illuminate/Database/Events/ModelsPruned.php Outdated Show resolved Hide resolved
@andreasnij
Copy link
Contributor

Nice! Would it be possible to make the models directory configurable?

@awebartisan
Copy link
Contributor

Great work on this Nuno.

@xiCO2k
Copy link
Contributor

xiCO2k commented Jul 2, 2021

That is awesome Nuno

@Gummibeer
Copy link
Contributor

Good one Nuno! 🚀

@antonkomarev
Copy link
Contributor

The only one thing I don't like - console command name. It looks like we are pruning database, not models. Eloquent allows us to store records anywhere, even in filesystem. What about artisan model:prune?

@Messhias
Copy link

Messhias commented Jul 2, 2021 via email

@efrain-salas
Copy link

It's a feature I've implemented manually in many applications. I think it would be great to have it built into the core.

@Gummibeer
Copy link
Contributor

@antonkomarev You could argue the same about db:seed.

@antonkomarev
Copy link
Contributor

db:seed command can be plain SQLs, it's standalone feature. db:prune can't, it's model related feature.

@inxilpro
Copy link
Contributor

inxilpro commented Jul 3, 2021

I seem to be in the minority here, but this feels more appropriate as a package, not something that needs to be in the framework. It looks great, but I don’t mind a quick composer install if I need it.

Copy link
Contributor

@SagarNaliyapara SagarNaliyapara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚢 🚀

@tuarrep
Copy link
Contributor

tuarrep commented Jul 4, 2021

Should we allow to customize the chunk size? If yes, at command level (applied at all models), or at model level?

Why not both?

Command wide option changes the default value globally and models wide option changes the value only for this model.

When both are provided the chunk size is either the model specified value if any either the command specified one if not.

That allows having a custom global default chuck size and also customizing it model by model.

@amitmerchant1990
Copy link
Contributor

Pretty handy!

@taylorotwell
Copy link
Member

Renamed command to model:prune.

@taylorotwell
Copy link
Member

Added configurable chunk size (on command level and model level).

@taylorotwell taylorotwell merged commit 437cbef into 8.x Jul 6, 2021
@taylorotwell taylorotwell deleted the feat/prunable-models branch July 6, 2021 20:14
@lupinitylabs
Copy link
Contributor

Looking at the implementation, I am wondering why there is a pruning() method, instead of just having pruning and pruned as model events for this to be in line with all the other events? 🤔

@fernfalcnico
Copy link

Nice work! I suggest clarifying that Laravel 8.5 is required.

GromNaN added a commit to mongodb/laravel-mongodb that referenced this pull request Sep 5, 2023
Custom implementation of MassPrunable is required to prevent using the limit. #2591 added an exception when limited is used because MongoDB Delete operation doesn't support it.

- MassPrunable::pruneAll() is called by the command model:prune.
- Using the parent trait is required because it's used to detect prunable models.
- Prunable feature was introducted in Laravel 8.x by laravel/framework#37889.

Users have to be aware that MassPrunable can break relationships as it doesn't call model methods to remove links.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet