404 Not Found

404 Not Found


nginx

قاعدة بيانات Laravel ونظام التهجير

التهجيرات هي "التحكم في إصدارات قاعدة البيانات" في Laravel — تدير هياكل قاعدة البيانات تمامًا كما يدير Git الكود، لذلك لم تعد الفرق بحاجة للقلق من عدم مزامنة هياكل الجداول.

1. ما ستتعلمه


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، تكون هياكل قاعدة البيانات متطابقة تمامًا.

BASH
php artisan make:migration add_discount_to_orders_table
# ينشئ ملف تهجير مؤقتًا
php artisan migrate
# يشغل جميع التهجيرات المعلقة بالترتيب

(3) العائد

بعد أن استبدل Charlie الـ SQL اليدوي بالتهجير، يجب أن تحدد الحقول الجديدة قيمًا افتراضية (يمكن اكتشافها أثناء مراجعة الكود). عندما يدفع Bob تغييراته المحلية إلى Git، يزامنها Charlie بأمر واحد، بدون أي حوادث.


3. أساسيات التهجير

(1) إنشاء ملف تهجير

BASH
# إنشاء تهجير
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) بنية ملف التهجير

PHP
// 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) ▶ مثال: إنشاء وتشغيل تهجير

BASH
# إنشاء تهجير
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

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

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) الفهارس والقيود

PHP
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

PHP
// 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']);
});

الناتج:

TEXT
// تم التنفيذ بنجاح

5. تصميم جداول متعددة المستأجرين

(1) الجداول الأساسية لـ ShopMetrics

100%
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

PHP
// 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');
});

الناتج:

TEXT
// تم التنفيذ بنجاح

6. استراتيجية تهجير الإنتاج

(1) مبادئ التهجير الآمن

المبدأ الوصف عواقب المخالفة
الإدخالات الجديدة يجب أن لها قيمة افتراضية ->default(0) أو ->nullable() خطأ للسجلات الموجودة
لا تحذف العمود؛ احذف الكود أولًا توقف عن استخدام العمود أولًا، ثم احذفه في الإصدار التالي الكود يشير إلى عمود غير موجود
إضافة أعمدة إلى الجدول باستخدام after() تقليل إعادة بناء الجدول بتحديد مواقع الأعمدة مدة قفل الجدول طويلة جدًا
حزمة التهجير ضمن معاملة خاصية withinTransaction نجاح جزئي، فشل جزئي

(2) الاختلافات بين MySQL وPostgreSQL

الميزة MySQL PostgreSQL
القيمة الافتراضية للعمود إضافة فورية تتطلب إعادة كتابة الجدول
عمود JSON json() json() + jsonb()
التعداد enum() يُنصح بـ string + CHECK
فهرس النص الكامل fullText() fullText() + GIN
فحص المفتاح الأجنبي يمكن تعطيله مؤقتًا فحص صارم

(1) ▶ مثال: إضافة عمود بأمان إلى جدول إنتاج كبير

PHP
// آمن: إضافة عمود مع قيمة افتراضية
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');
// });

الناتج:

TEXT
// تم التنفيذ بنجاح

7. تعديل هياكل الجداول

(1) تعديل الأعمدة

BASH
composer require doctrine/dbal
# مطلوب لتعديل الأعمدة الموجودة
PHP
Schema::table('shops', function (Blueprint $table) {
    $table->string('name', 100)->change();     // تغيير الطول
    $table->renameColumn('desc', 'description'); // إعادة تسمية العمود
    $table->dropColumn('legacy_field');          // حذف العمود
});

(2) تعديل الفهارس

PHP
Schema::table('orders', function (Blueprint $table) {
    $table->dropUnique('orders_order_number_unique');
    $table->unique(['tenant_id', 'order_number'], 'orders_tenant_order_unique');
});

(1) ▶ مثال: دليل عملي لتهجير وتخصيص ShopMetrics

PHP
// 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',
            ]);
        });
    }
};

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: مجموعة تهجيرات ShopMetrics الكاملة

PHP
// ============================================
// شامل: تهجيرات 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 في بيئة التطوير لأداء أسرع، لكن لا تستخدم أيًا منهما في بيئة الإنتاج.
س هل تؤثر قيود المفتاح الأجنبي على الأداء؟
ج نعم. يجب فحص قيود المفتاح الأجنبي عند كل عملية INSERT أو UPDATE أو DELETE. في سيناريوهات التزامن العالي، قد ترغب في التفكير في حذف قيود المفتاح الأجنبي وضمان تناسق البيانات بدلاً من ذلك على مستوى التطبيق.
س كيف أجري تهجيرًا بأمان في بيئة الإنتاج؟
ج أولًا، اختبر التهجير في بيئة التجهيز؛ الأعمدة الجديدة يجب أن يكون لها قيمة افتراضية أو تقبل null؛ حذف الأعمدة يجب أن يتم على خطوتين (أولًا أزل مراجع الكود، ثم احذف العمود في الإصدار التالي)؛ هجّر الجداول الكبيرة في ساعات الذروة المنخفضة.
س هل يمكن تعديل ملفات التهجير؟
ج التهجيرات غير المنفذة (أي غير المهجرة بعد) يمكن تعديلها بحرية؛ لا تعدل التهجيرات المنفذة بالفعل. بدلاً من ذلك، أنشئ تهجيرًا جديدًا لتنفيذ التغييرات. وإلا، سيعيد أمر migrate في البيئات الأخرى خطأ.
س ما مشاكل التوافق عند التهجير من SQLite إلى MySQL؟
ج SQLite لا يدعم بعض عمليات ALTER TABLE (مثل تغيير أنواع الأعمدة أو حذفها)، لذا استخدام SQLite أثناء التطوير قد يؤدي إلى قيود أثناء التهجير. نوصي باستخدام MySQL أو PostgreSQL في بيئات الإنتاج.
س كيف أضيف فهرسًا إلى جدول كبير موجود بدون قفل الجدول؟
ج MySQL يستخدم ALGORITHM=INPLACE LOCK=NONE (Laravel لا يدعم ذلك مباشرة؛ تحتاج لاستخدام DB::statement)؛ PostgreSQL يستخدم CONCURRENTLY (مدعوم في Laravel 11+ عبر $table->index('col')->concurrently()).

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ ملفات تهجير للجداول الثلاثة — tenants وplans وsubscriptions — لـ ShopMetrics، بما في ذلك المفاتيح الأجنبية والفهارس المناسبة، وشغّل migrate للتحقق من أن كل شيء صحيح.

  2. تمرين متقدم (⭐⭐): بناءً على جدول orders الموجود، أنشئ تهجيرًا لإضافة عمود discount (قيمة افتراضية 0) وفهرس مركب (tenant_id + status)، واكتب دالة down() المقابلة لضمان إمكانية التراجع عن التهجير.

  3. تحدٍ (⭐⭐⭐): صمم تهجير ربط متعدد الأشكال لـ ShopMetrics — اسمح لكل من المتاجر والمنتجات بأن يكون لها عناوين (باستخدام morphTo لجدول addresses) — ونفذ تصميم مفتاح أجنبي باستخدام morphable_type وmorphable_id.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%