راهنمای مهاجرت
اصول اساسی
مهاجرتها بخش مهمی از مدیریت پایگاه داده در برنامههای Laravel هستند. پیروی از این دستورالعملها تکامل روان پایگاه داده را تضمین میکند و از مشکلات رایج جلوگیری میکند.
مهاجرتهای با ساختار خوب، تغییرات پایگاه داده شما را قابل اعتماد، برگشتپذیر و قابل نگهداری در محیطهای مختلف میکند.
بهترین شیوههای مهاجرت
1. اصل مسئولیت واحد
هر مهاجرت باید فقط یک عملیات منطقی انجام دهد.
- شیوه خوب
- شیوه بد
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function up()
{
// ایجاد جدول
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
// درج داده در همان مهاجرت
DB::table('users')->insert([
'name' => 'Admin User',
'email' => 'admin@example.com',
'created_at' => now(),
'updated_at' => now(),
]);
}
عملیاتهای رایج با مسئولیت واحد شامل:
- ایجاد یک جدول جدید
- افزودن یک ستون به جدول موجود
- اصلاح دادههای موجود
- ایجاد شاخصها
2. نامگذاری توصیفی
- نامگذاری خوب
- نامگذاری بد
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}
3. همیشه متدهای Down را تعریف کنید
- متد Down کامل
- متد Down ناقص
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('users');
}
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
}
4. انواع مختلف عملیات را جدا کنید
- تغییرات ساختاری
- عملیات داده
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}
public function up()
{
$categories = [
['name' => 'Technology', 'slug' => 'technology'],
['name' => 'Health', 'slug' => 'health'],
['name' => 'Finance', 'slug' => 'finance'],
];
DB::table('categories')->insert($categories);
}
5. مدیریت مناسب وقفهها
Laravel به طور خودکار وقفهها را در مهاجرتها تشخیص نمیدهد، که میتواند منجر به وضعیتهای ناسازگار شود.
اگر یک مهاجرت با وقفه مواجه شود، Laravel ممکن است آن را به عنوان تکمیل شده علامتگذاری کند، حتی اگر اجرای آن به پایان نرسیده باشد.
راهحلها:
- بهینهسازی عملیاتهای دادهای بزرگ
- تقسیم مهاجرتهای بزرگ به موارد کوچکتر
- استفاده از پردازش تکهای برای مجموعه دادههای بزرگ
public function up()
{
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// پردازش هر کاربر
$user->update(['status' => 'active']);
}
});
}
6. تغییرناپذیری مهاجرتهای اجرا شده
پس از اعمال یک مهاجرت به پایگاه داده تولید، هرگز نباید آن را تغییر داد.
به جای اصلاح یک مهاجرت موجود، یک مهاجرت جدید برای ایجاد تغییرات اضافی ایجاد کنید.
7. هاردکد کردن Enum ها در مهاجرتها
همیشه مقادیر enum را به صورت هاردکد مستقیماً در مهاجرتها بنویس، به جای اینکه به کلاس enum ارجاع بدی.
این کار تضمین میکند که مهاجرتها پایدار و مستقل از تغییرات کد باقی بمانند. اگر در آینده یک کلاس enum تغییر کند یا حذف شود، مهاجرت همچنان به درستی کار میکند.
- شیوه خوب
- شیوه بد
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled']);
$table->timestamps();
});
}
use App\Enums\OrderStatus;
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->enum('status', OrderStatus::values());
$table->timestamps();
});
}
چرا این موضوع مهمه:
- مهاجرتها سوابق تاریخی هستند و باید تغییرناپذیر باشند
- کلاسهای enum ممکنه در طول زمان تغییر کنند و مهاجرتهای قدیمی رو خراب کنند
- مقادیر هاردکد شده تضمین میکنند که مهاجرتها در هر زمانی قابل اجرا باشند
8. استراتژیهای مدیریت کلید خارجی
رویکرد دو مرحلهای برای جداول موجود
هرگز یک کلید خارجی جدید را به یک جدول موجود در همان مایگریشنی که ستون آن را ایجاد میکند، اضافه نکنید. همیشه آن را در دو مایگریشن مجزا اعمال کنید تا ایمنی در بازگشت (Rollback) حفظ شده و از تشدید قفل (Lock Escalation) جلوگیری شود.
این رویکرد شامل دو مایگریشن جداگانه است:
- مایگریشن ۱: افزودن ستون: ستون جدید (مانند
customer_id) اضافه میشود اما به صورتnullable()تعریف میگردد. - مایگریشن ۲: افزودن محدودیت: محدودیت کلید خارجی به ستون اعمال میشود.
- مهاجرت ۱: افزودن ستون
- مهاجرت ۲: افزودن محدودیت
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->unsignedBigInteger('customer_id')->nullable()->after('id');
});
}
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('customer_id');
});
}
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->foreign('customer_id')
->references('id')
->on('customers')
->cascadeOnDelete();
});
}
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropForeign(['customer_id']);
});
}
این جداسازی به شما فرصت میدهد تا بین استقرارها دادهها را تکمیل کنید، زمان قطعی (Downtime) را به حداقل برسانید و اصل مسئولیت واحد را رعایت کنید.
استثنا: ایجاد جدول جدید
هنگام ایجاد یک جدول کاملاً جدید، تعریف کلیدهای خارجی در همان مایگریشن ایمن است. MySQL کل دستور CREATE TABLE، شامل محدودیتها، را در یک تراکنش واحد پردازش میکند. اگر هر بخشی با شکست مواجه شود، کل عملیات بازگردانده (Rollback) میشود.
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('title');
$table->text('body');
$table->timestamps();
// این کار ایمن است زیرا بخشی از ایجاد اولیه جدول است
$table->foreign('user_id')->references('id')->on('users');
});
}
قانون کلی برای تغییرات
برای هر سناریوی دیگری که شامل جداول موجود است، باید عملیات را به مایگریشنهای اتمی و جداگانه تقسیم کنید. هر یک از تغییرات زیر نیازمند فایل مایگریشن اختصاصی خود است:
- افزودن یک محدودیت کلید خارجی جدید.
- تغییر نام ستونی که قرار است کلید خارجی داشته باشد.
- تغییر نوع داده ستونی که قرار است کلید خارجی داشته باشد.
اگر نیاز به انجام چندین تغییر در ساختار یک ستون دارید (مثلاً تغییر نام، سپس افزودن ایندکس و سپس افزودن کلید خارجی)، برای هر عمل یک مایگریشن جداگانه ایجاد کنید. این کار حداکثر ایمنی و قابلیت بازگشت را تضمین میکند.
9. پاکسازی دادهها قبل از اعمال محدودیت
قبل از افزودن یک محدودیت foreign key یا unique به یک ستون موجود که دارای داده است، باید ابتدا یک مایگریشن برای پاکسازی دادهها اجرا کنید. استقرار بدون این مرحله میتواند منجر به شکستهای فاجعهبار در محیط تولید شود.
فرض کنید که ستون هدف حاوی دادههای نامعتبر است. مایگریشن پاکسازی شما باید این مشکلات را قبل از اعمال هرگونه محدودیت شناسایی و حل کند.
- برای کلیدهای خارجی: داده نامعتبر به معنای مقداری در ستون است که با هیچ شناسهای در جدول مرجع مطابقت ندارد (مثلاً
customer_idبرابر999در حالی که هیچ مشتری با این شناسه وجود ندارد). - برای کلیدهای یکتا: داده نامعتبر به معنای مقادیر تکراری در ستونی است که قصد دارید آن را یکتا کنید.
این مسئله به ویژه در پروژههای متعددی که ساختار پایگاه داده مشابه اما دادههای متفاوتی دارند، حیاتی است. یک مایگریشن ممکن است در محیط آزمایشی با دادههای تمیز موفقیتآمیز باشد، اما در حین استقرار در تولید، جایی که دادههای نامعتبر انباشته شدهاند، با شکست مواجه شود.
در اینجا یک فرآیند ایمن و سه مرحلهای برای افزودن کلید خارجی به ستونی که از قبل داده دارد، آمده است:
- مهاجرت ۱: پاکسازی دادهها
- مهاجرت ۲: افزودن ستون (در صورت نیاز)
- مهاجرت ۳: افزودن محدودیت
public function up()
{
// سفارشهایی را پیدا کنید که customer_id آنها در جدول customers وجود ندارد
// و آنها را به null تغییر دهید. استراتژی دیگر میتواند حذف آنها باشد.
DB::table('orders')
->whereNotNull('customer_id')
->whereNotExists(function ($query) {
$query->select(DB::raw(1))
->from('customers')
->whereColumn('customers.id', 'orders.customer_id');
})
->update(['customer_id' => null]);
}
public function down()
{
// این عملیات معمولاً غیرقابل بازگشت است، زیرا شناسههای نامعتبر اصلی را نمیدانیم.
// این موضوع را به وضوح مستند کنید.
}
این مرحله مشابه فاز اول ایجاد دو مرحلهای است. اگر ستون از قبل وجود دارد، میتوانید از این مایگریشن صرفنظر کنید.
public function up()
{
Schema::table('orders', function (Blueprint $table) {
// اطمینان حاصل کنید که ستون برای انتقال روان، nullable است
$table->unsignedBigInteger('customer_id')->nullable()->after('id');
});
}
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('customer_id');
});
}
پس از پاکسازی دادهها و اطمینان از وجود ستون، میتوانید با خیال راحت محدودیت کلید خارجی را اعمال کنید.
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->foreign('customer_id')
->references('id')
->on('customers')
->nullOnDelete(); // یا cascadeOnDelete()، بسته به رفتار مورد نظر
});
}
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropForeign(['customer_id']);
});
}
الگوهای رایج مهاجرت
افزودن یک ستون
- افزودن ستون
- افزودن چندین ستون
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone_number')->nullable()->after('email');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone_number');
});
}
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('address')->nullable()->after('email');
$table->string('city')->nullable()->after('address');
$table->string('country')->nullable()->after('city');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['address', 'city', 'country']);
});
}
تغییر نام یک ستون
- تغییر نام ستون
- تغییر نام چندین ستون
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('email', 'email_address');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('email_address', 'email');
});
}
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('first_name', 'given_name');
$table->renameColumn('last_name', 'family_name');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('given_name', 'first_name');
$table->renameColumn('family_name', 'last_name');
});
}
ایجاد یک شاخص
- شاخص تکی
- شاخص منحصر به فرد
public function up()
{
Schema::table('posts', function (Blueprint $table) {
$table->index(['user_id', 'created_at']);
});
}
public function down()
{
Schema::table('posts', function (Blueprint $table) {
$table->dropIndex(['user_id', 'created_at']);
});
}
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->unique('username');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropUnique('users_username_unique');
});
}
آزمایش مهاجرتها
- دستورات پایه
- دستورات پیشرفته
# بازنشانی پایگاه داده و اجرای تمام مهاجرتها
php artisan migrate:fresh
# برگرداندن آخرین دسته مهاجرتها
php artisan migrate:rollback
# برگرداندن تمام مهاجرتها و اجرای مجدد آنها
php artisan migrate:refresh
# برگرداندن و اجرای مجدد 5 مهاجرت آخر
php artisan migrate:refresh --step=5
# بازنشانی پایگاه داده بدون اجرای مهاجرتها
php artisan db:wipe
# نمایش وضعیت مهاجرت
php artisan migrate:status
نتیجهگیری
پیروی از این دستورالعملهای مهاجرت به حفظ ساختار پایگاه داده تمیز و قابل اعتماد در طول چرخه عمر برنامه شما کمک میکند. به یاد داشته باشید که مهاجرتها به عنوان یک سابقه تاریخی از تکامل پایگاه داده شما عمل میکنند، بنابراین وضوح و دقت ضروری است.