0

Lazy eager loading — load() after collection retrieval

Intermediate5 min read·lv-12-011
sqlperformance

Concept

Soft deletes allow "deleting" records without removing them from the database. A deleted_at timestamp column marks the deletion time. All standard queries automatically filter out soft-deleted records via a global scope.

Setup:

  1. Add deleted_at column in migration: $table->softDeletes().
  2. Add use SoftDeletes trait to the model.

What changes with SoftDeletes:

  • $model->delete() → sets deleted_at = NOW() instead of DELETE.
  • Model::all() / where() / find() → automatically adds WHERE deleted_at IS NULL.
  • Model::findOrFail($id) → returns 404 if the record is soft-deleted.

Accessing soft-deleted records:

  • Model::withTrashed(): Include soft-deleted in results.
  • Model::onlyTrashed(): Only soft-deleted records.
  • Model::withTrashed()->find($id): Find including soft-deleted.

Restore: $model->restore() → sets deleted_at = null.

Force delete: $model->forceDelete() → actual DELETE SQL, bypasses soft delete.

trashed() / isNotTrashed(): Check if a specific model instance is soft-deleted.

Relationships with soft deletes: By default, eager loaded relationships don't filter soft-deleted related records. Add ->withoutTrashed() or ->onlyTrashed() to relationship definitions if needed.

Cascading soft deletes: Not automatic. Implement in the deleting observer: $post->comments()->each->delete().

Code Example

php
<?php
// Migration
Schema::table('posts', function (Blueprint $table) {
    $table->softDeletes(); // adds nullable deleted_at timestamp column
});

// Model
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes;
}

// CRUD with soft deletes
$post = Post::find(1);
$post->delete();           // soft delete — sets deleted_at
// The post remains in the DB with deleted_at = NOW()

Post::find(1);             // null — hidden by global scope
Post::withTrashed()->find(1); // found — includes soft-deleted

$post->trashed();          // true — is soft-deleted
$post->restore();          // un-delete — deleted_at = null

$post->forceDelete();      // permanent DELETE SQL

// Querying
$activePosts = Post::all();                  // WHERE deleted_at IS NULL
$allPosts = Post::withTrashed()->get();      // includes deleted
$deletedPosts = Post::onlyTrashed()->get();  // only deleted

// Restoring multiple
Post::onlyTrashed()->where('user_id', 5)->restore();

// Cascade soft-delete to children
class User extends Model
{
    use SoftDeletes;

    protected static function boot(): void
    {
        parent::boot();
        static::deleting(function(User $user) {
            // Soft-delete all related posts too
            $user->posts()->each->delete();
        });
        static::restoring(function(User $user) {
            // When restoring user, restore soft-deleted posts within 5 min
            $user->posts()->withTrashed()
                ->where('deleted_at', '>=', now()->subMinutes(5))
                ->restore();
        });
    }
}

// Unique constraint on soft-deletable column — handle deleted duplicates
// Use Rule::unique()->whereNull('deleted_at') in validation
'email' => \Illuminate\Validation\Rule::unique('users')->whereNull('deleted_at'),