تُعتبر Laravel Migrations من أهم الأدوات التي يوفرها إطار Laravel لإدارة بنية قاعدة البيانات بطريقة منظمة وقابلة للتتبع.

يمكن النظر إلى Migration على أنها نوع من:

Version Control for Database Schema

فبدل أن يقوم كل مبرمج بإنشاء الجداول أو تعديل الأعمدة يدويًا، يتم تسجيل جميع التغييرات داخل ملفات Migration يمكن مشاركتها وتشغيلها والتراجع عنها بسهولة.

في هذا المقال سنستعرض مجموعة من أهم النصائح والأوامر المتعلقة بـLaravel Migration، بداية من إنشاء Foreign Keys وحتى Rollback وSoft Deletes وTimestamp وMigration Stubs وبعض الأدوات المفيدة في المشاريع الحقيقية.


1. إنشاء Foreign Key باستخدام foreignId()

في الإصدارات القديمة من Laravel كان من الشائع إنشاء Foreign Key على مرحلتين.

على سبيل المثال، إذا كان لدينا جدول:

books

وجدول:

reviews

وكان كل Review مرتبطًا بكتاب، فقد كنا نكتب:

$table->unsignedBigInteger('book_id');

$table->foreign('book_id')
    ->references('id')
    ->on('books');

أي أننا نقوم أولًا بإنشاء العمود:

book_id

ثم نقوم بإنشاء Foreign Key له.

الطريقة المختصرة

Laravel يوفر صيغة مختصرة وأكثر وضوحًا:

$table->foreignId('book_id')
    ->constrained();

يقوم:

foreignId('book_id')

بإنشاء عمود من النوع المناسب لـID التقليدي في Laravel، ثم تقوم:

constrained()

باستخدام Laravel naming conventions لمعرفة الجدول الذي يجب الربط به.

فمن:

book_id

يستنتج Laravel عادة:

books.id

الشكل النهائي

books
+----+----------------+
| id | title          |
+----+----------------+
| 1  | Laravel Guide  |
+----+----------------+


reviews
+----+---------+-------------+
| id | book_id | review      |
+----+---------+-------------+
| 1  | 1       | Excellent   |
| 2  | 1       | Very Good   |
+----+---------+-------------+

reviews.book_id
       │
       └───────────────→ books.id

2. استخدام $table->id()

بدل كتابة:

$table->bigIncrements('id');

يمكن استخدام:

$table->id();

وهي الطريقة المختصرة والأكثر شيوعًا في Laravel الحديث.

مثال:

Schema::create('books', function (Blueprint $table) {

    $table->id();

    $table->string('title');

    $table->timestamps();

});

3. استخدام foreignIdFor()

يوفر Laravel أيضًا طريقة أخرى مفيدة عند التعامل مع Models وهي:

foreignIdFor()

مثال:

use App\Models\Book;

$table->foreignIdFor(Book::class)
    ->constrained();

بدل كتابة:

$table->foreignId('book_id')
    ->constrained();

يمكن لهذه الطريقة أن تكون مفيدة لأنها تعتمد على الـModel نفسه.

إذا كان المشروع يستخدم UUID أو ULID كمفتاح أساسي، فراجع نوع المفتاح المستخدم قبل الاعتماد على foreignId() التقليدي. Laravel يوفر كذلك أدوات مثل foreignUuid() وforeignUlid().

4. ترتيب nullable() مع constrained()

إذا أردنا أن يكون Foreign Key اختياريًا ويسمح بالقيمة:

NULL

يجب وضع:

nullable()

قبل:

constrained()

الصحيح

$table->foreignId('book_id')
    ->nullable()
    ->constrained();

لا تعتمد على هذا الترتيب

$table->foreignId('book_id')
    ->constrained()
    ->nullable();
Laravel ينص على أن Column Modifiers الإضافية مثل nullable() يجب استدعاؤها قبل constrained().

5. ماذا يحدث عند حذف السجل الأب؟

لنفترض أن لدينا:

Book
  │
  ├── Review 1
  ├── Review 2
  └── Review 3

وقاعدة البيانات تحتوي:

reviews.book_id → books.id

إذا حاولنا حذف Book بينما توجد Reviews مرتبطة به، فإن سلوك قاعدة البيانات يعتمد على قاعدة البيانات المستخدمة وعلى الـForeign Key Action المحددة.

في MySQL مثلًا قد تحصل على خطأ مشابه:

Cannot delete or update a parent row:
a foreign key constraint fails

وهذا لأن قاعدة البيانات تحمي Referential Integrity ولا تريد ترك:

reviews.book_id

يشير إلى Book لم يعد موجودًا.


6. التحكم في onDelete

يمكن تحديد ما الذي يجب أن يحدث للـChild Records عندما يتم حذف Parent.

Laravel يوفر عدة خيارات.

CASCADE

عند حذف Book يتم حذف Reviews التابعة له تلقائيًا.

$table->foreignId('book_id')
    ->constrained()
    ->onDelete('cascade');

والصيغة الأكثر وضوحًا:

$table->foreignId('book_id')
    ->constrained()
    ->cascadeOnDelete();

النتيجة

DELETE Book

Book
 │
 ├── Review 1  ── Delete
 ├── Review 2  ── Delete
 └── Review 3  ── Delete

RESTRICT

يتم منع حذف Parent إذا كان يحتوي على Child Records مرتبطة به.

$table->foreignId('book_id')
    ->constrained()
    ->restrictOnDelete();

أي:

Book has Reviews
      │
      ▼
Delete Book
      │
      ▼
Rejected

SET NULL

بدل حذف Reviews، يتم تحويل:

book_id

إلى:

NULL

مثال:

$table->foreignId('book_id')
    ->nullable()
    ->constrained()
    ->nullOnDelete();

لاحظ أهمية:

nullable()

لأن قاعدة البيانات تحتاج أن يكون العمود قادرًا على استقبال:

NULL

قبل حذف Book

review.id      = 1
review.book_id = 5

بعد حذف Book رقم 5

review.id      = 1
review.book_id = NULL

NO ACTION

يوفر Laravel:

noActionOnDelete()

مثال:

$table->foreignId('book_id')
    ->constrained()
    ->noActionOnDelete();

السلوك الدقيق لـNO ACTION قد يختلف حسب نظام قاعدة البيانات المستخدم، لذلك لا يُفضل التعامل معه باعتباره مطابقًا دائمًا لـRESTRICT في جميع قواعد البيانات.


7. التحكم في onUpdate

يمكن تطبيق القواعد نفسها تقريبًا عند تحديث المفتاح المشار إليه.

مثال:

$table->foreignId('book_id')
    ->constrained()
    ->cascadeOnUpdate();

Laravel يوفر:

cascadeOnUpdate()

restrictOnUpdate()

nullOnUpdate()

noActionOnUpdate()

8. مثال كامل على Foreign Key

Schema::create('reviews', function (Blueprint $table) {

    $table->id();

    $table->foreignId('book_id')
        ->constrained()
        ->cascadeOnDelete();

    $table->text('review');

    $table->timestamps();

});

9. تحديد اسم جدول مختلف مع constrained()

Laravel يعتمد على Naming Conventions.

لكن إذا كان اسم الجدول لا يتبع تلك القواعد، يمكن تحديده يدويًا.

$table->foreignId('book_id')
    ->constrained('library_books');

في Laravel الحديث يمكن أيضًا استخدام Named Arguments:

$table->foreignId('book_id')
    ->constrained(
        table: 'library_books'
    );

10. معرفة Migrations التي تم تنفيذها

لمعرفة حالة جميع ملفات Migration نستخدم:

php artisan migrate:status

سيعرض Laravel قائمة بالـMigrations وحالتها.

يمكننا من خلالها معرفة:

  • أي Migrations تم تشغيلها.
  • أي Migrations ما زالت Pending.
  • الـBatch التي تنتمي إليها Migration.

وهذا الأمر مفيد جدًا عندما تحصل على مشروع من Git وتريد معرفة:

هل جميع التغييرات الموجودة داخل database/migrations تم تطبيقها بالفعل على قاعدة البيانات؟

11. مشاهدة SQL قبل تشغيل Migration

من الأدوات المفيدة جدًا، خصوصًا قبل تنفيذ تغييرات حساسة:

php artisan migrate --pretend

يتيح لك هذا الأمر مشاهدة SQL الذي ستقوم Migration بتنفيذه دون تطبيق التغييرات فعليًا.

هذه أداة مفيدة عند مراجعة Migrations قبل تشغيلها على قواعد البيانات المهمة.

12. استخدام useCurrent()

إذا أردنا إنشاء Timestamp يأخذ الوقت الحالي تلقائيًا كقيمة افتراضية:

$table->timestamp('reviewed_at')
    ->useCurrent();

وهذا يعادل مفهوم:

DEFAULT CURRENT_TIMESTAMP

تقريبًا على قواعد البيانات التي تدعمه.


13. استخدام useCurrentOnUpdate()

إذا أردنا تحديث Timestamp إلى الوقت الحالي كلما تم تعديل السجل:

$table->timestamp('reviewed_at')
    ->useCurrentOnUpdate();

يمكن أيضًا الجمع بينهما:

$table->timestamp('reviewed_at')
    ->useCurrent()
    ->useCurrentOnUpdate();
دعم useCurrentOnUpdate() يعتمد على Database Driver؛ Laravel يوثقه خصوصًا لـMySQL وMariaDB.

14. إضافة Soft Deletes إلى Migration

إذا كان Model يستخدم:

SoftDeletes

فيجب أن يحتوي الجدول عادة على:

deleted_at

ويمكن إنشاؤه من Migration بواسطة:

$table->softDeletes();

مثال:

Schema::create('cars', function (Blueprint $table) {

    $table->id();

    $table->string('model');

    $table->timestamps();

    $table->softDeletes();

});

سيضيف Laravel عمودًا مشابهًا لـ:

deleted_at TIMESTAMP NULL

15. تعديل Migration Stub الافتراضي

إذا كنت تضيف نفس الحقول في كل Migration جديدة، يمكنك تخصيص الـStubs التي يستخدمها Artisan لإنشاء الملفات.

نفذ:

php artisan stub:publish

سيقوم Laravel بنشر مجموعة من Stub Files داخل مجلد:

stubs/

في جذر المشروع.

المجلد في Laravel الحديث يكون عادة stubs في Root المشروع، وليس app/stubs.

بعد ذلك يمكنك تعديل Migration Stub المناسب.

على سبيل المثال:

Schema::create('{{ table }}', function (Blueprint $table) {

    $table->id();

    $table->timestamps();

    $table->softDeletes();

});

بعد ذلك، عندما تقوم بإنشاء Migration جديدة باستخدام:

php artisan make:migration create_cars_table

يمكن أن تحصل على:

Schema::create('cars', function (Blueprint $table) {

    $table->id();

    $table->timestamps();

    $table->softDeletes();

});
لاحظ أن الأمر الصحيح هو make:migration وليس make:migrate.

16. متى يكون تعديل Stub فكرة جيدة؟

يمكن أن يكون مفيدًا إذا كان المشروع يمتلك Convention ثابتة، مثل أن جميع الجداول تقريبًا تحتوي:

$table->id();

$table->timestamps();

$table->softDeletes();

أو حقول خاصة بالشركة.

لكن لا تضف حقولًا لكل Migration فقط لأنها مستخدمة في جدولين أو ثلاثة.

الـStub يجب أن يعكس قاعدة عامة داخل المشروع، وليس استثناءً.

17. حذف عمود من جدول

لنفترض أن جدول:

cars

يحتوي على:

model
number

لحذف عمود:

Schema::table('cars', function (Blueprint $table) {

    $table->dropColumn('model');

});

18. حذف عدة أعمدة في سطر واحد

Schema::table('cars', function (Blueprint $table) {

    $table->dropColumn([
        'model',
        'number'
    ]);

});

ملاحظة مهمة

عند تعديل جدول موجود نستخدم:

Schema::table()

وليس:

Schema::create()

لأن:

Schema::create()

مخصص لإنشاء جدول جديد.


19. الطريقة الصحيحة لاستخدام down()

وظيفة:

down()

هي عكس ما قامت به:

up()

إذا كان up() يضيف أعمدة

public function up(): void
{
    Schema::table('cars', function (Blueprint $table) {

        $table->string('model');

        $table->string('number');

    });
}

فيجب أن يحذفها down()

public function down(): void
{
    Schema::table('cars', function (Blueprint $table) {

        $table->dropColumn([
            'model',
            'number'
        ]);

    });
}

20. إذا كانت Migration تنشئ جدولًا كاملًا

يجب أن يكون:

public function up(): void
{
    Schema::create('cars', function (Blueprint $table) {

        $table->id();

        $table->string('model');

        $table->timestamps();

    });
}

وعكسها:

public function down(): void
{
    Schema::dropIfExists('cars');
}
قاعدة مهمة جدًا: اقرأ up() واسأل نفسك "كيف أعيد قاعدة البيانات إلى حالتها قبل هذا التغيير؟" هذا هو ما يجب أن يفعله down().

21. التراجع عن Migrations باستخدام rollback

يمكن التراجع عن آخر Migration Batch باستخدام:

php artisan migrate:rollback

من المهم فهم أن هذا يعني:

Last Batch

وليس بالضرورة:

Last Migration File

إذا تم تشغيل عدة Migrations في نفس عملية:

php artisan migrate

فقد تكون موجودة داخل نفس الـBatch.


22. التراجع باستخدام --step

إذا أردنا التراجع عن عدد محدد من Migrations:

php artisan migrate:rollback --step=3

وهذا يطلب من Laravel التراجع عن آخر ثلاث Migrations وفق سجل الـMigration.


23. استخدام migrate --step

يمكن كذلك تشغيل:

php artisan migrate --step

بحيث يتم وضع كل Migration في Batch مستقلة، ما قد يجعل عمليات Rollback الفردية أكثر مرونة لاحقًا.


24. migrate:refresh

الأمر:

php artisan migrate:refresh

يقوم بالتراجع عن Migrations ثم تشغيلها من جديد.

ويمكن:

php artisan migrate:refresh --step=3

للتراجع وإعادة تشغيل عدد محدد من أحدث Migrations.


25. migrate:reset

للتراجع عن جميع Migrations:

php artisan migrate:reset

26. migrate:fresh

يوجد فرق مهم بين:

migrate:refresh

و:

migrate:fresh

الأمر:

php artisan migrate:fresh

يقوم بحذف جميع الجداول ثم تشغيل Migrations من جديد.

ويمكن تشغيل Seeders:

php artisan migrate:fresh --seed
استخدم migrate:fresh بحذر شديد. هذا الأمر يحذف الجداول والبيانات، ولا ينبغي تشغيله على Production دون فهم كامل لما سيحدث.

27. تشغيل Migration في Production

عند تشغيل Migrations في بيئة Production قد يطلب Laravel تأكيدًا قبل تنفيذ عمليات حساسة.

في عمليات Deployment الآلية يمكن استخدام:

php artisan migrate --force
وجود --force لا يعني أن Migration آمنة. يجب مراجعة التغييرات والنسخ الاحتياطي وخطة Rollback قبل تنفيذ التعديلات الحساسة على Production.

28. تحديد بداية Auto Increment

عادة يبدأ:

$table->id();

من القيمة:

1

إذا أردنا أن يبدأ Auto Increment من رقم معين مثل:

1000

يمكن في قواعد البيانات المدعومة استخدام:

$table->id()
    ->from(1000);

مثال:

Schema::create('questions', function (Blueprint $table) {

    $table->id()
        ->from(1000);

    $table->string('question');

    $table->timestamps();

});
دعم from() مرتبط بنظام قاعدة البيانات؛ Laravel يوثقه لـMariaDB وMySQL وPostgreSQL.

29. إنشاء Migration باسم يحتوي على مسافات

عادة نستخدم:

php artisan make:migration add_phone_to_users_table

لكن يمكن أيضًا تمرير الاسم بين علامات اقتباس:

php artisan make:migration "add phone to users table"

ومع ذلك فإن استخدام:

snake_case

مثل:

add_phone_to_users_table

يبقى أوضح وأكثر شيوعًا داخل مشاريع Laravel.


30. إنشاء Migration مع تحديد الجدول

عند إضافة حقل إلى جدول موجود يمكن جعل الأمر أكثر وضوحًا:

php artisan make:migration add_phone_to_users_table --table=users

أما لإنشاء جدول جديد:

php artisan make:migration create_books_table --create=books

31. Schema Dump للمشاريع التي تحتوي على مئات Migrations

مع مرور السنوات قد يحتوي المشروع على مئات ملفات Migration.

Laravel يوفر:

php artisan schema:dump

ليقوم بإنشاء Schema File يمثل الحالة الحالية لقاعدة البيانات.

ويمكن استخدام:

php artisan schema:dump --prune

لإنشاء Schema Dump والتخلص من ملفات Migration القديمة وفق آلية Laravel.

هذه الميزة مفيدة خصوصًا للمشاريع الكبيرة التي أصبح تشغيل جميع Migrations التاريخية فيها بطيئًا.

32. حذف Foreign Key

إذا أردت حذف Foreign Key:

$table->dropForeign(
    'reviews_book_id_foreign'
);

أو باستخدام اسم العمود:

$table->dropForeign([
    'book_id'
]);

33. حذف Foreign Key والعمود

إذا كنت تريد إزالة العلاقة بالكامل:

Schema::table('reviews', function (Blueprint $table) {

    $table->dropForeign([
        'book_id'
    ]);

    $table->dropColumn(
        'book_id'
    );

});

وفي بعض الحالات يمكن استخدام Helpers المتاحة حسب نوع العمود وإصدار Laravel.


34. تعطيل Foreign Key Constraints مؤقتًا

Laravel يوفر:

Schema::disableForeignKeyConstraints();

ولإعادة تشغيلها:

Schema::enableForeignKeyConstraints();

كما يمكن حصر التعطيل داخل Closure:

Schema::withoutForeignKeyConstraints(
    function () {

        // operations

    }
);
تعطيل Foreign Keys ليس حلًا لمشاكل التصميم. استخدمه فقط عندما يكون لديك سبب واضح ومحدد.

35. الفرق بين Foreign Key وIndex

من الأخطاء الشائعة الاعتقاد أن:

Foreign Key

و:

Index

هما الشيء نفسه.

الـForeign Key وظيفته الأساسية الحفاظ على:

Referential Integrity

بينما Index وظيفته الأساسية تحسين الوصول إلى البيانات والاستعلامات.

مثال Index:

$table->index('status');

Unique Index:

$table->unique('email');

36. Composite Index

إذا كانت الاستعلامات تستخدم أكثر من حقل معًا باستمرار:

WHERE user_id = ?
AND status = ?

قد يكون من المناسب -بعد دراسة الاستعلامات- إنشاء Composite Index:

$table->index([
    'user_id',
    'status'
]);
لا تقم بإنشاء Index لكل عمود بشكل عشوائي. الـIndexes تحسن بعض عمليات القراءة لكنها أيضًا تستهلك مساحة وتزيد تكلفة عمليات الكتابة والتحديث.

37. Naming Convention مهم جدًا

Laravel يستطيع كتابة الكثير من الإعدادات نيابةً عنك إذا التزمت بتسمية واضحة.

على سبيل المثال:

Model:
Book

Table:
books

Primary Key:
id

Foreign Key:
book_id

يسمح لك ذلك بكتابة:

$table->foreignId('book_id')
    ->constrained();

دون الحاجة إلى تحديد:

references('id')
on('books')

38. مثال Migration احترافية كاملة

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create(
            'reviews',
            function (Blueprint $table) {

                $table->id();

                $table
                    ->foreignId('book_id')
                    ->constrained()
                    ->cascadeOnDelete();

                $table
                    ->unsignedTinyInteger('rating');

                $table
                    ->text('review')
                    ->nullable();

                $table
                    ->timestamp('reviewed_at')
                    ->useCurrent();

                $table->timestamps();

                $table->softDeletes();
            }
        );
    }


    public function down(): void
    {
        Schema::dropIfExists('reviews');
    }
};

39. أهم أوامر Laravel Migration

إنشاء Migration

php artisan make:migration create_books_table

تشغيل Migrations

php artisan migrate

معرفة الحالة

php artisan migrate:status

مشاهدة SQL دون التنفيذ

php artisan migrate --pretend

التراجع عن آخر Batch

php artisan migrate:rollback

التراجع عن عدد محدد

php artisan migrate:rollback --step=3

التراجع عن جميع Migrations

php artisan migrate:reset

Rollback ثم Migrate

php artisan migrate:refresh

حذف جميع الجداول ثم Migrate

php artisan migrate:fresh

حذف الجداول وتشغيل Seeders

php artisan migrate:fresh --seed

Production

php artisan migrate --force

نشر Stubs

php artisan stub:publish

إنشاء Schema Dump

php artisan schema:dump

40. نصائح مهمة قبل تشغيل Migration على Production

  • راجع SQL المتوقع قبل التغييرات الكبيرة.
  • خذ نسخة احتياطية من قاعدة البيانات.
  • انتبه عند حذف Columns أو Tables.
  • لا تعتمد على Rollback باعتباره Backup.
  • اختبر Migration على Staging أولًا.
  • انتبه إلى Locks عند تعديل الجداول الكبيرة.
  • لا تستخدم migrate:fresh على Production.
  • اجعل down() منطقيًا وقادرًا على عكس التغيير متى كان ذلك ممكنًا.

الخلاصة

Laravel Migrations ليست مجرد وسيلة لإنشاء الجداول، وإنما نظام كامل لإدارة تطور بنية قاعدة البيانات مع المشروع.

بدل كتابة Foreign Key بالطريقة المطولة:

$table->unsignedBigInteger('book_id');

$table->foreign('book_id')
    ->references('id')
    ->on('books');

يمكن غالبًا استخدام:

$table->foreignId('book_id')
    ->constrained();

ويمكن التحكم بسلوك الحذف:

cascadeOnDelete()

restrictOnDelete()

nullOnDelete()

noActionOnDelete()

كما يجب تذكر أن:

nullable()

يوضع قبل:

constrained()

ويمكن إدارة Migration History باستخدام:

migrate:status

migrate:rollback

migrate:refresh

migrate:fresh

بالإضافة إلى أدوات أكثر تقدمًا مثل:

migrate --pretend

migrate --step

schema:dump

stub:publish
الهدف من Migration ليس فقط أن تعمل قاعدة البيانات على جهازك، بل أن يستطيع أي مبرمج أو Server إعادة بناء نفس Database Schema بصورة موثوقة ومتوقعة من خلال الكود الموجود في المشروع.