درک کامل Migrations در جنگو:仕 نحوهٔ کار، دستورات، گردش‌کار و نکات مربوط به پایگاه‌داده‌ها

این مقاله یک توضیح جامع دربارهٔ Migrations در جنگو ارائه می‌دهد؛ اینکه چگونه تغییرات مدل‌ها را به ساختار پایگاه‌داده منتقل می‌کنند، چه دستورات مدیریتی برای کار با آن‌ها وجود دارد، گردش‌کار استاندارد چیست، و چه تفاوت‌هایی میان پایگاه‌داده‌های مختلف در اجرای مهاجرت‌ها وجود دارد.

مهاجرت دیتابیس، تغییرات مدلDjango migrations، makemigrationsmigrate، sqlmigrate، showmigrations

~11 دقیقه مطالعه • بروزرسانی ۲۳ اسفند ۱۴۰۴

مقدمه

Migrations در جنگو مکانیزمی برای انتقال تغییرات مدل‌ها به ساختار پایگاه‌داده هستند. هر تغییری که در مدل‌ها ایجاد می‌کنید—مثل افزودن فیلد، حذف مدل یا تغییر گزینه‌ها—باید در دیتابیس نیز اعمال شود. مهاجرت‌ها این فرآیند را خودکار، قابل‌ردگیری و قابل‌تکرار می‌کنند.


دستورات اصلی Migrations

جنگو چند دستور مدیریتی برای کار با مهاجرت‌ها ارائه می‌دهد:

  • migrate: اعمال یا بازگردانی مهاجرت‌ها روی دیتابیس
  • makemigrations: ساخت فایل‌های مهاجرت بر اساس تغییرات مدل‌ها
  • sqlmigrate: نمایش SQL مربوط به یک مهاجرت
  • showmigrations: نمایش لیست مهاجرت‌ها و وضعیت آن‌ها

به makemigrations مانند «کامیت»‌های نسخه‌سازی دیتابیس نگاه کنید و migrate را «اعمال» آن کامیت‌ها روی دیتابیس بدانید.


محل ذخیره‌سازی مهاجرت‌ها

هر اپلیکیشن جنگو یک پوشهٔ migrations/ دارد که فایل‌های مهاجرت در آن ذخیره می‌شوند. این فایل‌ها باید در سیستم کنترل نسخه (مثل Git) ذخیره شوند تا همهٔ محیط‌ها—توسعه، تست و تولید—ساختار دیتابیس یکسانی داشته باشند.

در صورت نیاز می‌توان مسیر ذخیرهٔ مهاجرت‌ها را با تنظیم MIGRATION_MODULES تغییر داد.


چگونه جنگو تغییرات را تشخیص می‌دهد؟

جنگو برای هر تغییری در مدل‌ها مهاجرت می‌سازد—even اگر آن تغییر روی دیتابیس تأثیر مستقیم نداشته باشد. این کار برای بازسازی کامل تاریخچهٔ مدل‌ها ضروری است و در مهاجرت‌های داده‌ای (Data Migrations) اهمیت زیادی دارد.


پشتیبانی پایگاه‌داده‌ها و محدودیت‌ها

PostgreSQL

بهترین پشتیبانی را برای مهاجرت‌ها دارد. اکثر عملیات‌ها تراکنشی و ایمن هستند.

MySQL

  • عدم پشتیبانی از تراکنش در تغییرات ساختار دیتابیس
  • در صورت شکست مهاجرت باید تغییرات را دستی بازگردانی کنید
  • محدودیت در اندازهٔ ایندکس‌ها
  • MySQL 8.0 سرعت DDL را بهبود داده اما همچنان ممکن است قفل‌گذاری رخ دهد

SQLite

SQLite پشتیبانی محدودی از تغییرات ساختار دارد. جنگو برای شبیه‌سازی مهاجرت‌ها:

  1. یک جدول جدید می‌سازد
  2. داده‌ها را منتقل می‌کند
  3. جدول قدیمی را حذف می‌کند
  4. جدول جدید را به نام قبلی تغییر نام می‌دهد

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


گردش‌کار استاندارد مهاجرت‌ها

۱. تغییر مدل‌ها

مثلاً افزودن یک فیلد یا حذف یک مدل.

۲. ساخت مهاجرت‌ها


$ python manage.py makemigrations

جنگو مدل‌ها را با مهاجرت‌های قبلی مقایسه کرده و فایل‌های جدید می‌سازد. خروجی را بررسی کنید تا مطمئن شوید تغییرات درست تشخیص داده شده‌اند.

۳. اعمال مهاجرت‌ها


$ python manage.py migrate

۴. کامیت کردن تغییرات

تغییرات مدل و فایل‌های مهاجرت را در یک کامیت ذخیره کنید تا همهٔ اعضای تیم هماهنگ باشند.

نام‌گذاری سفارشی مهاجرت‌ها


$ python manage.py makemigrations --name changed_my_model your_app

کنترل نسخه و تعارض‌ها

گاهی دو توسعه‌دهنده هم‌زمان برای یک اپلیکیشن مهاجرت می‌سازند و شمارهٔ مهاجرت‌ها تکراری می‌شود. نگران نباشید—شماره‌ها فقط برای راحتی هستند. جنگو از وابستگی‌ها برای تشخیص ترتیب صحیح استفاده می‌کند.

اگر تعارض وجود داشته باشد، جنگو به شما هشدار می‌دهد و در برخی موارد می‌تواند مهاجرت‌ها را خودکار خطی‌سازی کند. در غیر این صورت، باید فایل‌های مهاجرت را دستی ویرایش کنید—که معمولاً ساده است.


جمع‌بندی

Migrations یکی از ستون‌های اصلی معماری جنگو هستند. آن‌ها امکان توسعهٔ تدریجی و ایمن ساختار دیتابیس را فراهم می‌کنند. با شناخت دستورات، گردش‌کار، محدودیت‌های پایگاه‌داده‌ها و اصول کنترل نسخه، می‌توانید مهاجرت‌ها را با اطمینان و بدون دردسر مدیریت کنید.

تراکنش‌ها در Migrationها

در پایگاه‌داده‌هایی که از تراکنش‌های DDL پشتیبانی می‌کنند (مثل PostgreSQL و SQLite)، تمام عملیات یک Migration به‌صورت خودکار داخل یک تراکنش اجرا می‌شود. اما در دیتابیس‌هایی مثل MySQL و Oracle که از تراکنش DDL پشتیبانی نمی‌کنند، عملیات بدون تراکنش اجرا می‌شود.

غیرفعال کردن تراکنش برای یک Migration


class Migration(migrations.Migration):
    atomic = False

همچنین می‌توان بخش‌هایی از Migration را با atomic() یا RunPython(..., atomic=True) داخل تراکنش اجرا کرد.


وابستگی‌ها (Dependencies)

مهاجرت‌ها در سطح اپلیکیشن مدیریت می‌شوند، اما مدل‌ها معمولاً به هم وابسته‌اند. مثلاً اگر در اپ books یک ForeignKey به مدل Author اضافه کنید، مهاجرت مربوطه به مهاجرت اپ authors وابسته خواهد شد.

این وابستگی تضمین می‌کند که ابتدا جدول Author ساخته شود و سپس ستون ForeignKey ایجاد گردد.

نکتهٔ مهم:

  • اپ‌هایی که مهاجرت ندارند نباید به اپ‌هایی که مهاجرت دارند وابسته باشند.

وابستگی‌های قابل‌تعویض (Swappable Dependencies)

تابع swappable_dependency() برای مدل‌هایی استفاده می‌شود که قابل‌تعویض هستند، مثل مدل کاربر سفارشی:


swappable_dependency(settings.AUTH_USER_MODEL)

این کار به جنگو می‌گوید که مهاجرت به مدل قابل‌تعویض وابسته است و باید ابتدا مهاجرت مربوط به آن مدل اجرا شود.


فایل‌های Migration

فایل‌های مهاجرت در واقع فایل‌های پایتون هستند که شامل یک کلاس Migration با دو بخش اصلی‌اند:

  • dependencies: لیست مهاجرت‌هایی که باید قبل از این اجرا شوند
  • operations: لیست عملیات‌هایی که باید روی دیتابیس انجام شوند

نمونهٔ یک فایل مهاجرت


class Migration(migrations.Migration):
    dependencies = [("migrations", "0001_initial")]

    operations = [
        migrations.DeleteModel("Tribble"),
        migrations.AddField("Author", "rating", models.IntegerField(default=0)),
    ]

جنگو این عملیات‌ها را بررسی کرده و ساختار دیتابیس را بر اساس آن‌ها می‌سازد.


فیلدهای سفارشی و Migrationها

اگر فیلد سفارشی دارید، نمی‌توانید تعداد آرگومان‌های موقعیتی آن را تغییر دهید؛ زیرا مهاجرت‌های قبلی سازگار نخواهند بود. اگر نیاز به آرگومان جدید دارید، آن را به‌صورت keyword argument اضافه کنید.


مدیرهای مدل (Managers) در Migrationها

اگر می‌خواهید Manager سفارشی در Migrationها قابل‌استفاده باشد، باید ویژگی use_in_migrations را فعال کنید:


class MyManager(models.Manager):
    use_in_migrations = True

مهاجرت‌های اولیه (Initial Migrations)

مهاجرت‌های اولیه اولین نسخهٔ جداول یک اپ را می‌سازند. این مهاجرت‌ها با initial = True مشخص می‌شوند.

اگر از migrate --fake-initial استفاده کنید، جنگو بررسی می‌کند که آیا جداول یا ستون‌ها از قبل وجود دارند یا نه و در صورت وجود، مهاجرت را «فیک» اعمال می‌کند.


سازگاری تاریخچه (History Consistency)

اگر هنگام ادغام شاخه‌های توسعه، وابستگی‌ها را اشتباه تنظیم کنید، ممکن است مهاجرتی اعمال شده باشد اما وابستگی‌هایش اعمال نشده باشند. در این حالت جنگو اجازهٔ اجرای مهاجرت‌ها را نمی‌دهد تا مشکل رفع شود.


افزودن مهاجرت به اپ‌های قدیمی

اگر اپی دارید که مدل دارد اما مهاجرت ندارد، کافی است:


$ python manage.py makemigrations your_app
$ python manage.py migrate --fake-initial

این کار فقط زمانی درست عمل می‌کند که:

  • مدل‌ها از زمان ساخت جداول تغییر نکرده باشند
  • دیتابیس را دستی تغییر نداده باشید

بازگردانی مهاجرت‌ها (Reversing Migrations)

برای بازگردانی یک مهاجرت:


$ python manage.py migrate books 0002

برای بازگردانی تمام مهاجرت‌های یک اپ:


$ python manage.py migrate books zero

مهاجرت‌های غیرقابل‌بازگشت

اگر مهاجرت شامل عملیات غیرقابل‌بازگشت باشد (مثل DROP TABLE)، جنگو خطای IrreversibleError می‌دهد.


جمع‌بندی

Migrationها یکی از مهم‌ترین بخش‌های معماری جنگو هستند. با درک تراکنش‌ها، وابستگی‌ها، ساختار فایل‌های مهاجرت، مهاجرت‌های اولیه و نحوهٔ بازگردانی آن‌ها، می‌توانید دیتابیس پروژهٔ خود را با اطمینان و بدون خطا مدیریت کنید.

مدل‌های تاریخی در مهاجرت‌های جنگو

وقتی مهاجرت‌ها اجرا می‌شوند، جنگو از نسخه‌های تاریخی مدل‌ها استفاده می‌کند؛ یعنی نسخه‌هایی که در فایل‌های مهاجرت ذخیره شده‌اند، نه مدل‌های فعلی پروژه. این کار تضمین می‌کند که مهاجرت‌های قدیمی حتی پس از تغییر مدل‌ها همچنان قابل اجرا باشند.

چرا نباید مدل‌ها را مستقیم import کنیم؟

اگر در یک RunPython مدل‌ها را مستقیم import کنید، ممکن است:

  • در محیط‌های جدید مهاجرت‌ها شکست بخورند
  • مهاجرت‌های قدیمی دیگر قابل اجرا نباشند
  • ساخت دیتابیس از صفر با خطا مواجه شود

اگر چنین مشکلی رخ داد، کافی است مهاجرت را اصلاح کنید و از apps.get_model() استفاده کنید.

محدودیت‌های مدل‌های تاریخی

مدل‌های تاریخی:

  • متدهای سفارشی ندارند
  • متد save() سفارشی را اجرا نمی‌کنند
  • سازندهٔ سفارشی ندارند
  • فقط Managerهایی را دارند که use_in_migrations = True باشند
  • گزینه‌های Meta نسخهٔ تاریخی را دارند

بنابراین باید منطق مهاجرت را ساده و بدون وابستگی به رفتارهای سفارشی بنویسید.


حذف فیلدها و مشکلات مربوط به آن

اگر فیلد سفارشی را از پروژه حذف کنید، اما مهاجرت‌های قدیمی هنوز به آن فیلد اشاره کنند، مهاجرت‌ها شکست خواهند خورد. برای جلوگیری از این مشکل، جنگو سیستم هشدار و حذف تدریجی فیلدها را فراهم کرده است.

مرحلهٔ اول: اعلام منقضی شدن فیلد


class IPAddressField(Field):
    system_check_deprecated_details = {
        "msg": "این فیلد منقضی شده است.",
        "hint": "از GenericIPAddressField استفاده کنید.",
        "id": "fields.W900",
    }

مرحلهٔ دوم: اعلام حذف فیلد


class IPAddressField(Field):
    system_check_removed_details = {
        "msg": "این فیلد حذف شده است (به‌جز در مهاجرت‌های تاریخی).",
        "hint": "از GenericIPAddressField استفاده کنید.",
        "id": "fields.E900",
    }

تا زمانی که مهاجرت‌های قدیمی وجود دارند، باید متدهای پایهٔ فیلد مثل __init__()، deconstruct() و get_internal_type() را نگه دارید.


Data Migrationها

علاوه بر تغییر ساختار دیتابیس، می‌توانید داده‌ها را نیز تغییر دهید. این کار با RunPython انجام می‌شود.

ساخت یک مهاجرت خالی


python manage.py makemigrations --empty yourappname

مثال: ترکیب first_name و last_name


def combine_names(apps, schema_editor):
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = f"{person.first_name} {person.last_name}"
        person.save()

class Migration(migrations.Migration):
    dependencies = [
        ("yourappname", "0001_initial"),
    ]
    operations = [
        migrations.RunPython(combine_names),
    ]

اگر تابع معکوس ارائه نکنید، بازگردانی مهاجرت خطا خواهد داد.


دسترسی به مدل‌های سایر اپ‌ها

اگر در RunPython نیاز به مدل‌های اپ دیگری دارید، باید آخرین مهاجرت آن اپ را در dependencies اضافه کنید.

مثال


class Migration(migrations.Migration):
    dependencies = [
        ("app1", "0001_initial"),
        ("app2", "0004_foobar"),  # برای دسترسی به مدل‌های app2
    ]
    operations = [
        migrations.RunPython(move_m1),
    ]

جمع‌بندی

مدل‌های تاریخی، حذف تدریجی فیلدها و Data Migrationها از مهم‌ترین بخش‌های مهاجرت‌های پیشرفتهٔ جنگو هستند. با استفادهٔ صحیح از apps.get_model()، نگه‌داشتن فیلدهای قدیمی تا زمان مناسب، و نوشتن مهاجرت‌های داده‌ای ایمن، می‌توانید پروژه‌ای پایدار و قابل‌نگهداری داشته باشید.

مهاجرت‌های پیشرفته در جنگو

اگر به عملیات‌های پیچیده‌تر مهاجرت علاقه‌مند هستید یا می‌خواهید عملیات‌های سفارشی بنویسید، جنگو ابزارهای قدرتمندی برای این کار فراهم کرده است. در این مقاله به مهم‌ترین بخش‌های پیشرفتهٔ مهاجرت‌ها می‌پردازیم.


Squashing Migrations

در طول توسعهٔ پروژه، تعداد مهاجرت‌ها به‌مرور زیاد می‌شود. جنگو می‌تواند صدها مهاجرت را بدون مشکل اجرا کند، اما برای نگه‌داشت بهتر پروژه، گاهی لازم است آن‌ها را فشرده کنیم.

Squashing یعنی ترکیب چند مهاجرت در یک مهاجرت جدید که همان تغییرات را اعمال می‌کند.

جنگو چگونه Squash انجام می‌دهد؟

جنگو:

  1. تمام عملیات‌های مهاجرت‌های قبلی را استخراج می‌کند
  2. آن‌ها را پشت‌سرهم قرار می‌دهد
  3. یک Optimizer روی آن‌ها اجرا می‌کند
  4. عملیات‌های قابل‌حذف را حذف می‌کند (مثل CreateModel + DeleteModel)
  5. یک فایل مهاجرت جدید می‌سازد

اگر عملیات‌هایی مثل RunPython یا RunSQL وجود داشته باشند، ممکن است قابل‌بهینه‌سازی نباشند مگر اینکه elidable=True باشند.

مثال اجرای squashmigrations


$ python manage.py squashmigrations myapp 0004

پس از ساخت مهاجرت فشرده‌شده:

  • فایل جدید را commit کنید
  • فایل‌های قدیمی را حذف نکنید
  • پس از اینکه همهٔ محیط‌ها به نسخهٔ جدید مهاجرت کردند، فایل‌های قدیمی را حذف کنید

رفع خطای CircularDependencyError

اگر Squashing باعث ایجاد حلقهٔ وابستگی شود:

  • یکی از ForeignKeyها را در یک مهاجرت جداگانه قرار دهید
  • وابستگی‌ها را دستی اصلاح کنید

سریال‌سازی مقادیر در مهاجرت‌ها

مهاجرت‌ها فایل‌های پایتون هستند و باید بتوانند مقادیر را به‌صورت کد ذخیره کنند. جنگو بسیاری از انواع داده را می‌تواند سریال‌سازی کند، مثل:

  • اعداد، رشته‌ها، بولین‌ها
  • لیست، دیکشنری، tuple، set
  • datetime، date، time
  • UUID، Decimal
  • Enumها
  • Pathlib
  • LazyObject
  • توابع و متدهای سطح بالا (top-level)
  • هر فیلد جنگو
  • کلاس‌هایی که deconstruct() دارند

چیزهایی که جنگو نمی‌تواند سریال‌سازی کند

  • کلاس‌های تو در تو (Nested classes)
  • اشیای دلخواه مثل MyClass(4.3, 5.7)
  • Lambdaها

Serializerهای سفارشی

اگر نوع داده‌ای دارید که جنگو نمی‌تواند سریال‌سازی کند، می‌توانید Serializer سفارشی بسازید.

مثال: Serializer برای Decimal


class DecimalSerializer(BaseSerializer):
    def serialize(self):
        return repr(self.value), {"from decimal import Decimal"}

MigrationWriter.register_serializer(Decimal, DecimalSerializer)

استفاده از deconstruct() برای کلاس‌های سفارشی

اگر می‌خواهید یک کلاس سفارشی در مهاجرت‌ها سریال‌سازی شود، باید متد deconstruct() را پیاده‌سازی کنید.

ساختار خروجی deconstruct()

این متد باید سه مقدار برگرداند:

  1. path: مسیر کامل کلاس
  2. args: آرگومان‌های موقعیتی سازنده
  3. kwargs: آرگومان‌های کلیدی سازنده

مثال با decorator


@deconstructible
class MyCustomClass:
    def __init__(self, foo=1):
        self.foo = foo

    def __eq__(self, other):
        return self.foo == other.foo

Decorator به‌صورت خودکار مقادیر سازنده را ذخیره و در deconstruct() بازگردانی می‌کند.


پشتیبانی از چند نسخهٔ جنگو

اگر نگه‌دارندهٔ یک پکیج Third‑Party هستید و باید از چند نسخهٔ جنگو پشتیبانی کنید:

همیشه makemigrations را با پایین‌ترین نسخهٔ جنگویی که پشتیبانی می‌کنید اجرا کنید.

این کار تضمین می‌کند که مهاجرت‌ها در نسخه‌های بالاتر نیز بدون مشکل کار کنند.


جمع‌بندی

مهاجرت‌های پیشرفتهٔ جنگو ابزارهای قدرتمندی برای مدیریت پروژه‌های بزرگ و پیچیده ارائه می‌دهند. با استفاده از Squashing، سریال‌سازی سفارشی، deconstruct() و رعایت اصول پشتیبانی نسخه‌ای، می‌توانید مهاجرت‌هایی پایدار، تمیز و قابل‌نگهداری بسازید.

نوشته و پژوهش شده توسط دکتر شاهین صیامی