ميزة Soft Delete في Laravel: الحذف الناعم للسجلات

مقدمة

تُعد ميزة Soft Delete (الحذف الناعم) من أهم المميزات التي يوفرها إطار العمل Laravel للتعامل مع حذف السجلات دون فقدانها فعليًا من قاعدة البيانات.

عند تفعيل هذه الميزة على جدول معين، لا يُحذف السجل فعليًا عند استدعاء عملية الحذف، بل يُضاف حقل باسم deleted_at إلى الجدول، وتكون قيمته الافتراضية null. عند حذف السجل، لا يُزال من قاعدة البيانات، بل يتم تحديث هذا الحقل بقيمة timestamp تمثل وقت الحذف.

وبذلك:

  • إذا كانت قيمة deleted_at تساوي null → السجل غير محذوف.
  • إذا كانت قيمة deleted_at تحتوي على تاريخ → السجل محذوف، والتاريخ يمثل وقت الحذف.

في هذا المرجع سنستعرض كيفية تفعيل واستخدام Soft Delete خطوة بخطوة، بالاعتماد على جدول articles كمثال.


1. تفعيل Soft Delete في ملف Migration

عند إنشاء الجدول لأول مرة

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->softDeletes();
        $table->timestamps();
    });
}

عند إضافتها لجدول موجود مسبقًا

في حال كان جدول articles موجودًا مسبقًا، ونريد إضافة الحقل لاحقًا، ننشئ migration جديد:

php artisan make:migration add_soft_deletes_to_article_table --table="articles"
public function up()
{
    Schema::table('articles', function (Blueprint $table) {
        $table->softDeletes();
    });
}

ثم تنفيذ الأمر:

php artisan migrate

بهذا يتم إضافة حقل deleted_at إلى جدول articles.


2. تجهيز الـ Model

داخل Article Model، نستخدم الـ Trait الخاص بـ SoftDeletes:

class Article extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected $dates = ['deleted_at'];
}

3. جلب البيانات العادية

عند استخدام دوال الجلب الافتراضية مثل findOrFail، سيتم استثناء السجلات المحذوفة تلقائيًا (Laravel يضيف شرط WHERE deleted_at IS NULL ضمنيًا):

Route::get('/article/{article}', function ($article) {
    return Article::findOrFail($article);
});

4. جلب البيانات مع المحذوفة (withTrashed)

إذا أردنا جلب جميع السجلات، بما فيها المحذوفة، نستخدم withTrashed:

Route::get('/article/{article}', function ($article) {
    return Article::withTrashed()->findOrFail($article);
});

5. عرض السجلات المحذوفة فقط (onlyTrashed)

لعرض السجلات المحذوفة حصرًا، نستخدم onlyTrashed:

$articles = Article::onlyTrashed()->get();
return $articles;

6. استعادة السجلات المحذوفة (restore)

استعادة جميع السجلات المحذوفة

Article::onlyTrashed()->restore();

استعادة سجل معين

Route::get('/article/{article}', function ($article) {
    return Article::withTrashed()->findOrFail($article)->restore();
});

7. الحذف النهائي للسجلات (forceDelete)

بما أن السجل لا يُحذف فعليًا عند استخدام Soft Delete، فإن حذفه بشكل كامل ونهائي من قاعدة البيانات يتم عبر forceDelete:

حذف جميع السجلات المحذوفة نهائيًا

Article::onlyTrashed()->forceDelete();

حذف السجلات المحذوفة منذ فترة معينة فقط (مثلاً أقدم من 30 يومًا)

Article::onlyTrashed()
    ->where('deleted_at', '<', Carbon::now()->subDays(30))
    ->forceDelete();

حذف سجل محدد نهائيًا

Article::onlyTrashed()->find(2)->forceDelete();

8. تمرير withTrashed مباشرة على الـ Route (Laravel 8.55+)

ابتداءً من Laravel 8.55، أصبح بالإمكان تمرير withTrashed مباشرة على تعريف الـ Route عند استخدام Route Model Binding، بدلًا من استدعائها داخل جسم الدالة:

Route::get('/article/{article}', function (Article $article) {
    return $article;
})->withTrashed();

وفي حال كان السجل محذوفًا، ستتم إعادته مع القيمة الظاهرة في حقل deleted_at:

{
    "id": 3,
    "title": "Sed iusto eius quis.",
    "created_at": "2021-09-11T07:59:39.000000Z",
    "updated_at": "2021-09-11T10:14:39.000000Z",
    "deleted_at": "2021-09-11T10:14:39.000000Z"
}

جدول ملخّص للدوال

الدالةالوظيفة
softDeletes()إضافة حقل deleted_at في الـ migration
SoftDeletes (Trait)تفعيل السلوك داخل الـ Model
withTrashed()جلب جميع السجلات بما فيها المحذوفة
onlyTrashed()جلب السجلات المحذوفة فقط
restore()استعادة سجل أو سجلات محذوفة
forceDelete()حذف السجل نهائيًا من قاعدة البيانات

الخلاصة

توفّر ميزة Soft Delete في Laravel طبقة أمان إضافية عند التعامل مع حذف البيانات، حيث تمنح المطور مرونة في:

  • حذف السجلات دون فقدانها فعليًا.
  • استعادتها لاحقًا عند الحاجة عبر restore().
  • عرضها بشكل منفصل عبر onlyTrashed() أو تضمينها عبر withTrashed().
  • حذفها نهائيًا عند التأكد من عدم الحاجة إليها عبر forceDelete().

هذه الميزة مفيدة بشكل خاص في الأنظمة التي تتطلب إمكانية التراجع عن الحذف، أو الاحتفاظ بسجل تاريخي للبيانات المحذوفة لأغراض التدقيق (Audit).