عنوان:

‫الگوهای بهینه در مدیریت EF Core Migrations


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۳ ۰۸:۴۰
آدرس: www.dntips.ir
چکیده: مکانیزم Migrations در فریم‌ورک Entity Framework Core ابزاری قدرتمند برای تکامل تدریجی طرح‌واره پایگاه‌داده (Database Schema) همگام با مدل دامنه دات‌نت است. با این حال، تکیه بر تنظیمات پیش‌فرض در محیط‌های عملیاتی سازمانی (Production) چالش‌هایی نظیر از دست رفتن داده‌ها (Data Loss)، عدم همخوانی نسخه مدل، قفل شدن جداول و ناسازگاری در معماری‌های چندنسخه‌ای ایجاد می‌کند. این مقاله راهکارهای استاندارد، الگوهای نام‌گذاری، تغییرات ساختاری ایمن، استراتژی‌های استقرار در خطوط CI/CD، روش‌های صحیح مقداردهی اولیه داده‌ها (Data Seeding) و مانیتورینگ سلامت را بررسی می‌کند.

مقدمه
توسعه نرم‌افزارهای مدرن بر پایه تکامل پیوسته کالبد داده شکل می‌گیرد. در اکوسیستم دات‌نت، ابزار EF Core Migrations تغییرات کلاس‌های #C را شناسایی کرده و به کدهای دستکاری ساختار (DDL) ترجمه می‌کند. سهولت ایجاد مایگریشن‌ها در محیط توسعه اغلب این تصور اشتباه را ایجاد می‌کند که مدیریت پایگاه‌داده نیازی به مداخله و بررسی دقیق ندارد؛ اما در محیط عملیاتی، هر دستور مایگریشن معادل مستقیم اسکریپت‌های SQL با اثرات غیرقابل‌بازگشت است. شناخت عمیق نحوه عملکرد، فایل‌های پشتیبان مانند Snapshot و محدودیت‌های این ابزار، شرط بنیادین پایداری سیستم‌های نرم‌افزاری است.

۱. اصول طراحی و ساختاربندی مایگریشن‌ها

نام‌گذاری دقیق و توصیفی
مایگریشن‌ها تاریخچه زنده کالبد داده شما هستند. استفاده از نام‌های مبهم مانند Fix یا Update پس از چند ماه پیگیری تاریخچه را غیرممکن می‌سازد. از نام‌هایی شفاف، فعل‌محور و معطوف به هدف تغییر استفاده کنید:
  • AddUserTable
  • AddMiddleNameToUserTable
  • RemoveSalesDateFromClientOrderTable

تفکیک دغدغه‌ها (One Migration Per Change)
هر مایگریشن باید یک تغییر منطقی منفرد را در بر گیرد. ترکیب تغییرات نامرتبط مانند افزودن ستون به جدول User، تغییر ساختار جدول Sales و نگاشت توابع، فرآیند بازبینی کد (Code Review) و بازگردانی احتمالی (Rollback) را با اختلال مواجه می‌کند.

۲. عملیات پرخطر و تغییرات بدون خرابی (Zero-Downtime Schema Changes)
در محیط‌های عملیاتی با داده‌های واقعی، تغییر نوع داده، اندازه یا تغییر نام ستون‌ها جزو عملیات پرخطر (Dangerous Operations) دسته‌بندی می‌شوند:
[ستون اصلی] ──(EF Core Drop)──> [حذف کامل ستون و داده‌ها] ──(Add Column)──> [ایجاد ستون جدید با ساختار جدید]

استراتژی موازی (Parallel Change / Expand-Contract)
به جای تغییر مستقیم فیلد، رویکرد فازبندی‌شده را در پیش بگیرید:
  • Expand: افزودن ستون جدید با نام یا نوع جدید در کنار ستون قبلی.
  • Dual-Write / Sync: نگارش در هر دو فیلد یا همگام‌سازی با تریگر/بک‌گراند سرویس.
  • Backfill: انتقال داده‌های قدیمی به فیلد جدید.
  • Contract: منسوخ کردن فیلد قبلی و حذف نهایی آن در نسخه‌های بعدی.

تغییر نام ستون با متدRenameColumn()
در صورت تغییر نام ویژگی در سی‌شارپ، EF Core به‌طور پیش‌فرض ستون قبلی را حذف و ستون جدید می‌سازد. برای جلوگیری از دست رفتن داده، مایگریشن تولیدشده را بازبینی کرده و از متد migrationBuilder.RenameColumn() استفاده کنید.
نکته حیاتی: ویوها، پروسیجرها و ایندکس‌های سفارشی که به نام قبلی وابسته‌اند، به‌طور خودکار به‌روزرسانی نمی‌شوند و باید در اسکریپت مایگریشن مدنظر قرار گیرند.

۳. چرخه حیات و اصلاح مایگریشن‌ها
ساختار EF Core وابسته به فایل‌های تولیدشده مایگریشن و فایل ModelSnapshot.cs است. دستکاری دستی فایل‌های تولیدشده بدون درک ساختار اسنپ‌شات، هماهنگی مدل و پایگاه‌داده را برهم می‌زند.

روش‌های استاندارد اصلاح:
حذف آخرین مایگریشن اِعمال‌نشده:
dotnet ef migrations remove

بازگردانی مایگریشن اِعمال‌شده در پایگاه‌داده:
  • ابتدا وضعیت پایگاه‌داده را به نسخه قبل از تغییر برگردانید و سپس اقدام به حذف فایل کنید:
dotnet ef database update <PreviousMigrationName>
dotnet ef migrations remove

تغییرات در میانه زنجیره:
  • اگر تغییرات در محیط‌های دیگر اِعمال شده‌اند، هرگز فایل‌های گذشته را حذف نکنید؛ بلکه با افزودن مایگریشن جدید (Forward-Fixing) ساختار را به حالت مطلوب ببرید.

۴. اجرای مایگریشن در پروژه‌های چندلایه‌ای
در معماری‌های تمیز و چندلایه‌ای، DbContext معمولاً در پروژه‌ای مجزا (نظیر لایه Persistence یا Data) نسبت به پروژه اجرایی (API) قرار دارد. برای اجرای دستورات CLI باید پروژه استارتاپ را صراحتاً مشخص کنید:
# اجرا از پوشه پروژه Data
dotnet ef migrations add AddMiddleNameToUser --startup-project ../MyApp.ApiService
dotnet ef database update --startup-project ../MyApp.ApiService

۵. استراتژی‌های استقرار در محیط عملیاتی
استفاده از متد Database.Migrate() یا MigrateAsync() در متد آغازین برنامه (Program.cs) به دلایل زیر در محیط‌های سازمانی منسوخ و پرخطر است:
  • تضاد در محیط‌های چندنسخه‌ای (Multi-Instance/Horizontal Scaling): همزمانی در اجرای مایگریشن و ایجاد قفل‌های همپوشان.
  • نقض اصل کمترین سطح دسترسی (Principle of Least Privilege): نیازمند بودن دسترسی سطح DDL (مانند ALTER TABLE) برای کاربری وب‌اپلیکیشن در دیتابیس.
  • تأخیر در راه‌اندازی (Startup Timeout): توقف بالا آمدن کانتینر در صورت طولانی شدن فرآیند ساخت ایندکس‌های سنگین.

راهکارهای جایگزین و استاندارد:
روش استقرارموارد کاربردمزایا
Migration Bundlesکانتینرها، خطوط ابری CI/CDفایل اجرایی منفرد، مستقل از کد، سبک و بدون نیاز به نصب .NET SDK کامل
Idempotent SQL Scriptsخطوط سنتی، کنترل توسط DBAاسکریپت شفاف، امکان بازبینی پیش از اجرا و بررسی جدول __EFMigrationsHistory
ساخت Migration Bundle:
dotnet ef migrations bundle --self-contained -r linux-x64 -o efbundle
# اجرا در فرآیند استقرار
./efbundle --connection "Server=...;Database=...;"

تولید اسکریپت امن و Idempotent:
dotnet ef migrations script --idempotent -o update.sql

۶. الگوهای مقداردهی اولیه داده‌ها (Data Seeding)

داده‌های مدل و ثابت (Model Seed Data)
داده‌هایی مانند نقش‌های پایه یا لیست کشورها که بخشی از هویت سیستم هستند، مستقیماً در متد OnModelCreating با استفاده از HasData() تعریف می‌شوند:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Country>().HasData(
        new Country { Id = 1, Name = "Iran", Code = "IR" },
        new Country { Id = 2, Name = "Germany", Code = "DE" }
    );
}
  • الزام کلید اصلی: در HasData مقدار کلید اصلی (Primary Key) حتماً باید مشخص شود تا EF Core بتواند تغییرات رکوردها را در مایگریشن‌های بعدی شناسایی کند. تغییر شناسه موجود معادل حذف و ثبت مجدد خواهد بود.

داده‌های محیط توسعه و تست (Test Seed Data)
از ترکیب EnsureCreatedAsync() با سیستم مایگریشن‌ها پرهیز کنید، چرا که این متد جدول __EFMigrationsHistory را نادیده گرفته و پایگاه‌داده را از وضعیت سازگار با مایگریشن خارج می‌کند. الگوی صحیح، تفکیک منطق تست در یک کلاس مجزا و اجرای آن پس از اِعمال مایگریشن‌ها در محیط Development است:
public static class TestDataSeeder
{
    public static async Task SeedAsync(AppDbContext db)
    {
        if (!await db.Users.AnyAsync())
        {
            db.Users.AddRange(
                new User { Name = "Alice", Email = "alice@example.com" },
                new User { Name = "Bob", Email = "bob@example.com" }
            );
            await db.SaveChangesAsync();
        }
    }
}

// Program.cs
if (app.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync();
    await TestDataSeeder.SeedAsync(db);
}

۷. بررسی سلامت و مایگریشن‌های معلق (Pending Migrations)
اگر فرآیند اعمال مایگریشن را از چرخه اجرای اپلیکیشن جدا کنید، برای اطمینان از همخوانی طرح‌واره دیتابیس با آخرین تغییرات کدهای مستقرشده، از سیستم Health Check استاندارد ASP.NET Core استفاده نمایید:
builder.Services.AddHealthChecks()
    .AddCheck<MigrationHealthCheck>("database_migrations");

public class MigrationHealthCheck(AppDbContext db) : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        var pending = (await db.Database.GetPendingMigrationsAsync(ct)).ToList();

        return pending.Count == 0
            ? HealthCheckResult.Healthy("Database schema is fully synchronized.")
            : HealthCheckResult.Degraded($"{pending.Count} pending migration(s): {string.Join(", ", pending)}");
    }
}

۸. تجمیع مایگریشن‌ها (Squashing Migrations)
در طول چرخه‌های طولانی توسعه، تجمع صدها فایل مایگریشن سرعت بارگذاری مدل و اجرای دستورات را کاهش می‌دهد. در صورت تمایل به یکپارچه‌سازی تاریخچه:
[مایگریشن ۱] ──> [مایگریشن ۲] ──> ... ──> [مایگریشن ۱۰۰]
                         ↓
               [یکپارچه‌سازی (Squash)]
                         ↓
       [مایگریشن پایه نهایی با ثبت وضعیت در تاریخچه]
  • ایجاد یک مایگریشن خالی پایانی و اِعمال آن بر تمامی محیط‌های موجود.
  • ذخیره نام و برچسب زمانی (Timestamp) آن مایگریشن.
  • حذف تمام فایل‌های مایگریشن قبلی از Solution و پاکسازی اسنپ‌شات.
  • تولید یک مایگریشن نوظهور با اسامی یکسان با برچسب مرحله اول.
  • به این ترتیب محیط‌های قدیمی تغییر جدیدی دریافت نکرده و محیط‌های جدید دیتابیس را به طور کامل از روی فایل تجمیع‌شده می‌سازند.

نتیجه‌گیری
مایگریشن‌های EF Core در حقیقت ساختار کدنویسی‌شده همان اسکریپت‌های SQL هستند. تسلط بر نحوه ترجمه دستورات LINQ/C# به DDL در پایگاه‌داده، تفکیک مایگریشن‌های هر فیچر، بهره‌گیری از Migration Bundles در خطوط CI/CD و پایش وضعیت مایگریشن‌های معلق، تضمین‌کننده تکامل ایمن، بدون قطعی و پایدار پایگاه‌داده در محیط‌های بزرگ‌مقیاس دات‌نت خواهد بود.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۵/۲۳ ۰۸:۴۶
    یک نکته‌ی تکمیلی: تجمیع مایگریشن‌ها (Squashing Migrations) یکی از مباحث پیشرفته و حساس در نگهداری بلندمدت پروژه‌های مبتنی بر EF Core است.

    زمانی که یک پروژه چند سال فعال است یا در جریان یک بازنویسی سنگین صدها مایگریشن برای افزودن، حذف یا تغییر فیلدها ایجاد می‌شود، حجم بالای این فایل‌ها سرعت بیلد، اجرای تست‌ها و زمان پردازش مدل را کاهش می‌دهد. در این شرایط، هدف از Squashing این است که تاریخچه طولانی و خرد مایگریشن‌ها به یک مایگریشن اولیه و یکپارچه (Initial/Baseline) تبدیل شود.

    چالش اصلی چیست؟
    اگر شما صرفاً تمام فایل‌های مایگریشن قبلی را پاک کنید و یک مایگریشن جدید به نام InitialCreate بسازید:
    • در دیتابیس جدید: همه‌چیز عالی کار می‌کند و دیتابیس با آخرین ساختار ساخته می‌شود.
    • در دیتابیس‌های عملیاتی (Production/Staging): با خطای بحرانی مواجه می‌شوید! چون EF Core جدول __EFMigrationsHistory را چک می‌کند؛ این جدول نام تک‌تک مایگریشن‌های قدیمی را دارد اما مایگریشن جدید شما (InitialCreate) را ندارد. در نتیجه تلاش می‌کند تمام جداول را از اول بسازد و با خطای Table already exists یا تخریب دیتابیس متوقف می‌شود.

    راهکار استاندارد EF Core برای تجمیع بدون شکستن دیتابیس‌های موجود
    برای اینکه دیتابیس‌های عملیاتی متوجه شوند که نیازی به اجرای مجدد ساختار ندارند، از تکنیک هماهنگ‌سازی تاریخچه استفاده می‌شود:

    1. ایجاد مایگریشن نهایی و خالی (Checkpoint Migration): پیش از دست زدن به فایل‌های قدیمی، ابتدا مطمئن شوید تمام محیط‌های عملیاتی به آخرین وضعیت مدل آپدیت شده‌اند. سپس یک مایگریشن خالی به عنوان نقطه پایان (Checkpoint) بسازید:
    dotnet ef migrations add CheckpointSync
    این مایگریشن هیچ تغییری در کالبد ایجاد نمی‌کند اما یک رکورد با Timestamp مشخص تولید می‌کند. نام کامل این فایل (شامل عدد تاریخ/زمان ابتدای آن، مثلاً 20260814050000_CheckpointSync) را یادداشت کنید.

    2. اِعمال مایگریشن نهایی روی تمامی محیط‌های موجود: دستور زیر را روی تمام دیتابیس‌های عملیاتی، تست و توسعه اجرا کنید تا نام این مایگریشن در جدول __EFMigrationsHistory ثبت شود:
    dotnet ef database update

    3. حذف فیزیکی مایگریشن‌های قبلی: تمام فایل‌های مایگریشن موجود در پوشه Migrations پروژه (از جمله فایل مایگریشن مرحله ۱) را حذف کنید. دقت کنید: فقط فایل Snapshot یا کدهای اصلی دیتابیس را دستکاری نکنید، صرفاً فایل‌های لیست مایگریشن‌ها را پاک کنید.

    4. ایجاد یک مایگریشن جامع جدید: اکنون دستور ساخت مایگریشن جدید را صادر کنید تا تمام مدل فعلی در قالب یک فایل منفرد تجمیع شود:
    dotnet ef migrations add InitialBaseline

    5. تطبیق نام و Timestamp مایگریشن جدید با Checkpoint: نام فایل، نام کلاس داخل فایل #C و همچنین مقدار شناسه درون فایل Snapshot تولیدشده را تغییر دهید تا دقیقاً برابر با همان نام و Timestamp مرحله اول (20260814050000_CheckpointSync) شود.

    نتیجه این فرآیند چیست؟
    برای پایگاه‌داده‌های موجود (Production):
    • وقتی برنامه را بالا می‌آورید، EF Core جدول __EFMigrationsHistory را بررسی می‌کند. می‌بیند که رکورد 20260814050000_CheckpointSync قبلاً ثبت و اجرا شده است؛ بنابراین هیچ کدی اجرا نمی‌کند و دیتابیس دست‌نخورده باقی می‌ماند.
    برای پایگاه‌داده‌های جدید (New Deployments / Local Test DBs):
    • دیتابیس تازه هیچ رکوردی در جدول تاریخچه ندارد؛ بنابراین همین یک مایگریشن تجمیع‌شده را از ابتدا اجرا می‌کند و تمام جداول را دقیقاً مطابق با آخرین مدل می‌سازد.

    چه زمانی باید مایگریشن‌ها را Squash کنیم؟
    • انتشار نسخه ماژور (Major Release): زمانی که نسخه جدیدی از محصول ارائه می‌شود و می‌خواهید تمام تغییرات نسخه‌های قبل را پاکسازی و یکدست کنید.
    • کاهش زمان بیلد و تست: در پروژه‌های بزرگ با صدها مایگریشن، سرعت ایجاد دیتابیس موقت برای تست‌های یکپارچگی (Integration Tests) به شدت افزایش می‌یابد.
    • تغییرات پرشمار در فاز توسعه (قبل از Production): در شاخه‌های فیچر پرحجم، تجمیع مایگریشن‌ها پیش از Merge به برنچ اصلی (Main) تاریخچه‌ای تمیز به جا می‌گذارد.