قاعدة بيانات Laravel ونظام التهجير
التهجيرات هي "التحكم في إصدارات قاعدة البيانات" في Laravel — تدير هياكل قاعدة البيانات تمامًا كما يدير Git الكود، لذلك لم تعد الفرق بحاجة للقلق من عدم مزامنة هياكل الجداول.
1. ما ستتعلمه
- إنشاء وتشغيل ملفات التهجير: make:migration / migrate / rollback
- باني Schema: أنواع الأعمدة والفهارس وقيود المفاتيح الأجنبية
- تصميم جداول متعددة المستأجرين: tenants/users/subscriptions/plans/orders
- استراتيجية التهجير: تهجير آمن لبيئة الإنتاج بدون توقف
- التعامل مع الاختلافات في تهجيرات MySQL وPostgreSQL
2. قصة حقيقية لمسؤول قاعدة بيانات
(1) المشكلة: تنفيذ SQL يدويًا يسبب حوادث إنتاج
نفذ Charlie يدويًا استعلام ALTER TABLE orders ADD COLUMN discount DECIMAL(8,2) في بيئة الإنتاج لكنه نسي تضمين القيمة الافتراضية — ونتيجة لذلك، تم تعيين عمود "discount" لجميع سجلات المليونين إلى NULL، مما تسبب في تعطل وحدة التقارير لمدة ساعتين. لزيادة الطين بلة، أضاف Bob حقلًا محليًا دون إخبار Charlie، وعند نشر الكود، ظهر خطأ SQL فورًا.
(2) حلول تهجيرات Laravel
تستخدم تهجيرات Laravel كود PHP لوصف تغييرات قاعدة البيانات؛ تشارك الفرق ملفات التهجير، ويُتبع ترتيب التنفيذ تلقائيًا — بعد أن يشغل الجميع php artisan migrate، تكون هياكل قاعدة البيانات متطابقة تمامًا.
php artisan make:migration add_discount_to_orders_table
# ينشئ ملف تهجير مؤقتًا
php artisan migrate
# يشغل جميع التهجيرات المعلقة بالترتيب
(3) العائد
بعد أن استبدل Charlie الـ SQL اليدوي بالتهجير، يجب أن تحدد الحقول الجديدة قيمًا افتراضية (يمكن اكتشافها أثناء مراجعة الكود). عندما يدفع Bob تغييراته المحلية إلى Git، يزامنها Charlie بأمر واحد، بدون أي حوادث.
3. أساسيات التهجير
(1) إنشاء ملف تهجير
# إنشاء تهجير
php artisan make:migration create_shops_table
# مع تلميح اسم الجدول
php artisan make:migration create_shops_table --create=shops
# إضافة أعمدة إلى جدول موجود
php artisan make:migration add_status_to_shops_table --table=shops
(2) بنية ملف التهجير
// database/migrations/2024_01_15_000000_create_shops_table.php
return new class extends Migration
{
public function up(): void
{
Schema::create('shops', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->enum('status', ['active', 'suspended', 'closed'])->default('active');
$table->decimal('revenue', 12, 2)->default(0);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('shops');
}
};
(3) أوامر التهجير
| الأمر | الوظيفة |
|---|---|
migrate |
تنفيذ التهجيرات غير المشغلة |
migrate:rollback |
التراجع عن التهجير السابق |
migrate:refresh |
التراجع عن الكل + إعادة التنفيذ |
migrate:fresh |
حذف قاعدة البيانات + إعادة التنفيذ |
migrate:status |
عرض حالة التهجير |
migrate:reset |
التراجع عن جميع التهجيرات |
(1) ▶ مثال: إنشاء وتشغيل تهجير
# إنشاء تهجير
php artisan make:migration create_shops_table
# تشغيل التهجيرات المعلقة
php artisan migrate
# 2024_01_15_000000_create_shops_table .............. done
# التحقق من حالة التهجير
php artisan migrate:status
# تم؟ التهجير
# نعم 0001_01_01_000000_create_users_table
# نعم 2024_01_15_000000_create_shops_table
# لا 2024_01_16_000000_create_products_table
الناتج:
# تم تنفيذ الأمر بنجاح
4. باني Schema
(1) أنواع الأعمدة الشائعة
| الطريقة | نوع قاعدة البيانات | الوصف |
|---|---|---|
id() |
BIGINT UNSIGNED AUTO_INCREMENT | مفتاح أساسي |
foreignId('x') |
BIGINT UNSIGNED | مفتاح أجنبي |
string('name', 255) |
VARCHAR | سلسلة نصية |
text('content') |
TEXT | نص طويل |
integer('count') |
INT | عدد صحيح |
decimal('price', 8, 2) |
DECIMAL(8,2) | رقم عشري دقيق |
boolean('active') |
TINYINT(1) | قيمة منطقية |
enum('status', [...]) |
ENUM | تعداد |
json('metadata') |
JSON | بيانات JSON |
timestamp('published_at') |
TIMESTAMP | طابع زمني |
softDeletes() |
TIMESTAMP NULL | حذف ناعم |
(2) الفهارس والقيود
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->string('order_number')->unique();
$table->decimal('total', 12, 2);
// فهارس
$table->index('shop_id'); // فهرس واحد
$table->index(['shop_id', 'status']); // فهرس مركب
$table->unique(['tenant_id', 'order_number']); // فريد مركب
// قيود المفتاح الأجنبي
$table->foreignId('user_id')
->constrained('users') // اسم جدول مخصص
->cascadeOnDelete() // حذف متسلسل عند حذف الأب
->cascadeOnUpdate(); // تحديث متسلسل عند تحديث الأب
$table->timestamps();
});
| طريقة القيد | الوظيفة |
|---|---|
unique() |
فهرس فريد |
index() |
فهرس عادي |
constrained() |
استنتاج علاقة المفتاح الأجنبي تلقائيًا |
cascadeOnDelete() |
حذف متسلسل |
restrictOnDelete() |
تقييد الحذف (خطأ إذا وُجدت سجلات فرعية) |
nullOnDelete() |
تعيين إلى NULL عند حذف سجل أب |
(1) ▶ مثال: تصميم المفاتيح الأجنبية متعددة المستأجرين لـ ShopMetrics
// database/migrations/create_tenants_table.php
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->unique();
$table->enum('status', ['active', 'suspended', 'cancelled'])->default('active');
$table->timestamps();
$table->softDeletes();
});
// database/migrations/create_subscriptions_table.php
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('plan_id')->constrained()->cascadeOnDelete();
$table->enum('status', ['active', 'past_due', 'cancelled'])->default('active');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'status']);
});
الناتج:
// تم التنفيذ بنجاح
5. تصميم جداول متعددة المستأجرين
(1) الجداول الأساسية لـ ShopMetrics
timeline
title الخط الزمني لتهجيرات ShopMetrics
إنشاء tenants : جدول tenants
إنشاء plans : جدول plans
إنشاء users : جدول users مع tenant_id
إنشاء subscriptions : جدول subscriptions
إنشاء shops : جدول shops مع tenant_id
إنشاء products : جدول products مع shop_id
إنشاء categories : جدول categories
إنشاء category_product : جدول محوري
إنشاء orders : جدول orders مع tenant_id + shop_id
إنشاء order_items : جدول order_items مع order_id + product_id
(2) تسلسل التهجير الكامل
| الترتيب | اسم الجدول | الحقول الرئيسية |
|---|---|---|
| 1 | tenants | id، name، slug، domain، status |
| 2 | plans | id، name، price، features (JSON) |
| 3 | users | id، tenant_id (FK)، name، email، role |
| 4 | subscriptions | id، tenant_id (FK)، plan_id (FK)، status |
| 5 | shops | id، tenant_id (FK)، name، slug، revenue |
| 6 | products | id، shop_id (FK)، name، price، sku |
| 7 | categories | id، name، slug |
| 8 | category_product | category_id (FK)، product_id (FK) |
| 9 | orders | id، tenant_id (FK)، shop_id (FK)، total، status |
| 10 | order_items | id، order_id (FK)، product_id (FK)، qty، price |
(1) ▶ مثال: تهجير جدول المستخدمين الكامل لـ ShopMetrics
// database/migrations/create_users_table.php
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->enum('role', ['super_admin', 'tenant_owner', 'analyst', 'viewer'])
->default('viewer');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->index(['tenant_id', 'role']);
$table->index('email');
});
الناتج:
// تم التنفيذ بنجاح
6. استراتيجية تهجير الإنتاج
(1) مبادئ التهجير الآمن
| المبدأ | الوصف | عواقب المخالفة |
|---|---|---|
| الإدخالات الجديدة يجب أن لها قيمة افتراضية | ->default(0) أو ->nullable() |
خطأ للسجلات الموجودة |
| لا تحذف العمود؛ احذف الكود أولًا | توقف عن استخدام العمود أولًا، ثم احذفه في الإصدار التالي | الكود يشير إلى عمود غير موجود |
إضافة أعمدة إلى الجدول باستخدام after() |
تقليل إعادة بناء الجدول بتحديد مواقع الأعمدة | مدة قفل الجدول طويلة جدًا |
| حزمة التهجير ضمن معاملة | خاصية withinTransaction |
نجاح جزئي، فشل جزئي |
(2) الاختلافات بين MySQL وPostgreSQL
| الميزة | MySQL | PostgreSQL |
|---|---|---|
| القيمة الافتراضية للعمود | إضافة فورية | تتطلب إعادة كتابة الجدول |
| عمود JSON | json() |
json() + jsonb() |
| التعداد | enum() |
يُنصح بـ string + CHECK |
| فهرس النص الكامل | fullText() |
fullText() + GIN |
| فحص المفتاح الأجنبي | يمكن تعطيله مؤقتًا | فحص صارم |
(1) ▶ مثال: إضافة عمود بأمان إلى جدول إنتاج كبير
// آمن: إضافة عمود مع قيمة افتراضية
Schema::table('orders', function (Blueprint $table) {
$table->decimal('discount', 8, 2)
->default(0)
->after('total');
});
// آمن: جعل العمود يقبل null أولًا
Schema::table('shops', function (Blueprint $table) {
$table->string('phone')->nullable()->after('email');
});
// خطير: إزالة عمود — افعل ذلك على خطوتين
// الخطوة 1: هذا الإصدار — توقف عن استخدام العمود في الكود
// Schema::table('shops', function (Blueprint $table) {
// $table->dropColumn('legacy_field');
// });
الناتج:
// تم التنفيذ بنجاح
7. تعديل هياكل الجداول
(1) تعديل الأعمدة
composer require doctrine/dbal
# مطلوب لتعديل الأعمدة الموجودة
Schema::table('shops', function (Blueprint $table) {
$table->string('name', 100)->change(); // تغيير الطول
$table->renameColumn('desc', 'description'); // إعادة تسمية العمود
$table->dropColumn('legacy_field'); // حذف العمود
});
(2) تعديل الفهارس
Schema::table('orders', function (Blueprint $table) {
$table->dropUnique('orders_order_number_unique');
$table->unique(['tenant_id', 'order_number'], 'orders_tenant_order_unique');
});
(1) ▶ مثال: دليل عملي لتهجير وتخصيص ShopMetrics
// database/migrations/2024_02_01_add_stripe_to_subscriptions.php
return new class extends Migration
{
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->string('stripe_id')->nullable()->unique()->after('id');
$table->string('stripe_status')->nullable()->after('status');
$table->timestamp('current_period_start')->nullable()->after('trial_ends_at');
$table->timestamp('current_period_end')->nullable()->after('current_period_start');
});
}
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn([
'stripe_id', 'stripe_status',
'current_period_start', 'current_period_end',
]);
});
}
};
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: مجموعة تهجيرات ShopMetrics الكاملة
// ============================================
// شامل: تهجيرات ShopMetrics الأساسية
// يغطي: الجداول، المفاتيح الأجنبية، الفهارس، متعدد الأشكال
// ============================================
// التهجير 1: إنشاء جدول plans
Schema::create('plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->decimal('price', 8, 2);
$table->integer('shop_limit')->default(5);
$table->integer('order_limit')->default(1000);
$table->json('features')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
// التهجير 2: إنشاء جدول tenants
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->unique();
$table->foreignId('plan_id')->nullable()->constrained()->nullOnDelete();
$table->enum('status', ['active', 'suspended', 'cancelled'])->default('active');
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'created_at']);
});
// التهجير 3: إنشاء جدول shops
Schema::create('shops', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug');
$table->text('description')->nullable();
$table->decimal('revenue', 12, 2)->default(0);
$table->enum('status', ['active', 'suspended', 'closed'])->default('active');
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'slug']);
$table->index(['tenant_id', 'status']);
});
// التهجير 4: إنشاء جدول products
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('sku')->unique();
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['shop_id', 'is_active']);
});
// التهجير 5: إنشاء جدول orders
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('order_number')->unique();
$table->decimal('subtotal', 12, 2);
$table->decimal('discount', 8, 2)->default(0);
$table->decimal('total', 12, 2);
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])->default('pending');
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'status']);
$table->index(['shop_id', 'created_at']);
});
❓ أسئلة شائعة
migrate:fresh وmigrate:refresh؟fresh يحذف قاعدة البيانات ثم يعيد بناءها؛ أسرع لكن يؤدي إلى فقدان كامل للبيانات. refresh يتراجع عن التهجيرات أولًا ثم يطبقها، ينفذ down() ثم up() بالتسلسل. استخدم fresh في بيئة التطوير لأداء أسرع، لكن لا تستخدم أيًا منهما في بيئة الإنتاج.migrate في البيئات الأخرى خطأ.ALTER TABLE (مثل تغيير أنواع الأعمدة أو حذفها)، لذا استخدام SQLite أثناء التطوير قد يؤدي إلى قيود أثناء التهجير. نوصي باستخدام MySQL أو PostgreSQL في بيئات الإنتاج.ALGORITHM=INPLACE LOCK=NONE (Laravel لا يدعم ذلك مباشرة؛ تحتاج لاستخدام DB::statement)؛ PostgreSQL يستخدم CONCURRENTLY (مدعوم في Laravel 11+ عبر $table->index('col')->concurrently()).📖 ملخص
- التهجيرات هي شكل من أشكال التحكم في الإصدارات لقواعد البيانات؛ تشارك الفرق ملفات التهجير لضمان تناسق الهيكل
- باني Schema يصف هياكل الجداول باستخدام كود PHP ويدعم أنواع الأعمدة والفهارس والمفاتيح الأجنبية
- قيود المفتاح الأجنبي تضمن تكامل المرجعية، لكن في سيناريوهات التزامن العالي، قد ترغب في الاعتماد على طبقة التطبيق لضمانها
- تصميم الجداول متعددة المستأجرين يحقق عزل البيانات عبر مفتاح
tenant_idالأجنبي - تهجيرات الإنتاج يجب أن تكون آمنة: أضف أعمدة جديدة بقيم افتراضية، واحذف الأعمدة على خطوتين
- migrate:fresh: للتطوير؛ استخدم فقط "migrate" في الإنتاج
📝 تمارين
-
تمرين أساسي (⭐): أنشئ ملفات تهجير للجداول الثلاثة — tenants وplans وsubscriptions — لـ ShopMetrics، بما في ذلك المفاتيح الأجنبية والفهارس المناسبة، وشغّل
migrateللتحقق من أن كل شيء صحيح. -
تمرين متقدم (⭐⭐): بناءً على جدول
ordersالموجود، أنشئ تهجيرًا لإضافة عمودdiscount(قيمة افتراضية 0) وفهرس مركب (tenant_id+status)، واكتب دالةdown()المقابلة لضمان إمكانية التراجع عن التهجير. -
تحدٍ (⭐⭐⭐): صمم تهجير ربط متعدد الأشكال لـ ShopMetrics — اسمح لكل من المتاجر والمنتجات بأن يكون لها عناوين (باستخدام
morphToلجدولaddresses) — ونفذ تصميم مفتاح أجنبي باستخدامmorphable_typeوmorphable_id.



