راهنمای کامل انتقال یک Model در Django و PostgreSQL
راهنمای کامل انتقال یک Model در Django و PostgreSQLمثال واقعی: انتقال مدل Consultation از اپ account به اپ core در محیط Production / Liveهدفانتقال مدل و تمام دادهها بدون از دست دادن رکوردها، حفظ IDها، هماهنگکردن Sequence، اصلاح ارجاعات کد و در نهایت حذف مدل قدیمی.فهرست مراحلشناخت وضعیت اولیه و وابستگیهاساخت Safety Copyایجاد Model در app مقصدساخت و اجرای Migrationانتقال مستقیم دادههاهماهنگکردن Sequenceمقایسه دقیق دادههااصلاح Referenceهای کدحذف Model و جدول قدیمینهاییکردن related_nameتست Production و خطایابینگهداری و حذف Safety Copy۱. وضعیت اولیه مثالدر پروژه Django به نام nahast، مدل Consultation ابتدا داخل app account بود. مدل Website در app core قرار داشت؛ بنابراین ForeignKey مدل Consultation به جدول core_website اشاره میکرد.مدل اولیه:class Consultation(models.Model):
website = models.ForeignKey(
Website,
blank=True,
null=True,
on_delete=models.CASCADE,
related_name='consultations',
verbose_name='وب سایت'
)
name = models.CharField(
max_length=200,
blank=True,
null=True,
verbose_name='نام و نام خانوادگی'
)
mobile = models.CharField(
max_length=16,
verbose_name='تلفن تماس'
)
description = models.TextField(
null=True,
blank=True,
verbose_name='توضیحات'
)
note = models.TextField(
blank=True,
null=True,
verbose_name='یادداشت مدیر سایت'
)
status = models.BooleanField(
default=False,
verbose_name='وضعیت'
)
def __str__(self):
return self.mobileجدول قدیمیpublic.account_consultationجدول مقصدpublic.core_consultationتعداد رکوردهای مثال29نکته مهم قبل از شروع: وقتی روی محیط Live / Production کار میکنیم، مهمترین اصل این است: اول امکان برگشت داشته باش، بعد عملیات مخرب انجام بده. بنابراین قبل از حذف جدول قدیمی، یک Safety Copy از آن میسازیم. همچنین Migrationهای قبلی را دستکاری نکن و اگر تغییری بعداً لازم شد، یک Migration جدید بساز.۲. ساخت Safety Copy در PostgreSQLاز داخل PGAdmin وارد Query Tool شدیم. ابتدا یک کپی از ساختار جدول و سپس تمام دادهها ساختیم:CREATE TABLE public.account_consultation_backup
(LIKE public.account_consultation INCLUDING ALL);
INSERT INTO public.account_consultation_backup
SELECT *
FROM public.account_consultation;خروجی مثال: INSERT 0 29 یعنی 29 رکورد داخل Backup کپی شده است.بعد تعداد رکوردهای Backup را بررسی کردیم:SELECT COUNT(*)
FROM public.account_consultation_backup;نتیجه: 29. بنابراین مطمئن شدیم Safety Copy با تعداد رکوردهای جدول اصلی برابر است.۳. ایجاد Model در app مقصدمدل Consultation را از account/models.py به core/models.py اضافه کردیم. اما چون در آن لحظه مدل قدیمی هنوز در account وجود داشت، اگر هر دو مدل related_name="consultations" داشتند ممکن بود Django با Clash مواجه شود. بنابراین موقتاً در مدل جدید استفاده کردیم از:related_name="core_consultations"یعنی:website = models.ForeignKey(
Website,
blank=True,
null=True,
on_delete=models.CASCADE,
related_name="core_consultations",
verbose_name="وب سایت"
)بعد از اینکه مدل قدیمی حذف شد، related_name را دوباره به مقدار اصلی برگرداندیم: related_name="consultations"۴. ساخت و اجرای Migration مقصدبعد از اضافهکردن مدل به core/models.py:python manage.py makemigrations coreدر مثال Migration زیر ساخته شد: core/migrations/0005_consultation.py. این Migration شامل ساخت Model جدید بود. بعد:python manage.py migrate core 0005خروجی موفق: Applying core.0005_consultation... OK. از این لحظه جدول جدید ساخته شده بود (core_consultation)، اما هنوز دادهای داخل آن منتقل نکرده بودیم.۵. انتقال مستقیم دادههاچون تصمیم گرفتیم انتقال را مستقیماً داخل PostgreSQL انجام دهیم، از INSERT ... SELECT استفاده کردیم. SQL نهایی:INSERT INTO public.core_consultation
(id, name, mobile, description, note, status, website_id)
SELECT
id,
name,
mobile,
description,
note,
status,
website_id
FROM public.account_consultation;خروجی: INSERT 0 29 یعنی 29 رکورد منتقل شد.بررسی تعداد رکوردهابعد از انتقال:SELECT COUNT(*) FROM public.account_consultation;
SELECT COUNT(*) FROM public.core_consultation;در مثال نتیجه: account_consultation = 29 و core_consultation = 29. پس تعداد رکوردها برابر بود. اما این بهتنهایی کافی نیست؛ چرا؟ چون ممکن است هر دو جدول 29 رکورد داشته باشند، ولی محتوای بعضی رکوردها متفاوت باشد. برای همین مرحله بعد (مقایسه دقیق) لازم است.۶. چرا باید Sequence را تنظیم کنیم؟این یکی از مهمترین قسمتهای انتقال Model است. در PostgreSQL معمولاً ستون id به یک Sequence متصل است. Sequence یک شمارنده مستقل است که PostgreSQL از آن برای تولید IDهای جدید استفاده میکند. ممکن است IDهای موجود 1 تا 29 باشند ولی Sequence هنوز روی مقدار قدیمی (مثلاً 1) باشد. در این حالت اگر Django بخواهد یک رکورد جدید ایجاد کند، ممکن است PostgreSQL یک ID قدیمی تولید کند. نتیجه احتمالی: duplicate key value violates unique constraint.تنظیم SequenceSELECT setval(
pg_get_serial_sequence('public.core_consultation', 'id'),
COALESCE((SELECT MAX(id) FROM public.core_consultation), 1),
true
);در مثال خروجی 30 بود. چرا؟ چون MAX(id) = 29 و پارامتر سوم true است، یعنی PostgreSQL مقدار 29 را آخرین مقدار استفادهشده در نظر میگیرد و مقدار بعدی را 30 خواهد ساخت.نکته خیلی مهم درباره Sequence: این دو مورد را با هم اشتباه نگیریم. تعداد رکوردها (SELECT COUNT(*)) مثلاً 29؛ بزرگترین ID (SELECT MAX(id)) مثلاً 29؛ اما Sequence وظیفهاش تولید ID بعدی است. یعنی Sequence تعداد رکوردهای جدول نیست و MAX(id) هم لزوماً تعداد رکوردها نیست. مثلاً اگر رکوردهای ما 10، 20، 30 باشند، تعداد رکوردها 3 است ولی MAX(id) = 30 است.۷. مقایسه دقیق دادههای مبدأ و مقصدحالا که جدول ساخته شده، داده منتقل شده، تعداد رکوردها برابر است و Sequence تنظیم شده، باید مطمئن شویم خود دادهها نیز دقیقاً یکسان هستند. برای این کار از Query زیر استفاده کردیم:SELECT
a.id,
a.name,
a.mobile,
a.description,
a.note,
a.status,
a.website_id
FROM public.account_consultation a
FULL OUTER JOIN public.core_consultation c
ON a.id = c.id
WHERE
a.id IS NULL
OR c.id IS NULL
OR a.name IS DISTINCT FROM c.name
OR a.mobile IS DISTINCT FROM c.mobile
OR a.description IS DISTINCT FROM c.description
OR a.note IS DISTINCT FROM c.note
OR a.status IS DISTINCT FROM c.status
OR a.website_id IS DISTINCT FROM c.website_id;نتیجه در مثال: خروجی کاملاً خالی بود. یعنی هیچ رکورد متفاوتی وجود ندارد و هر 29 رکورد در جدول جدید دقیقاً مطابق جدول قبلی بودند.چرا از IS DISTINCT FROM استفاده کردیم؟چون در PostgreSQL مقایسه معمولی با NULL میتواند نتیجه غیرمنتظره بدهد. مثلاً NULL = NULL به صورت معمول TRUE نمیشود، اما NULL IS DISTINCT FROM NULL نتیجه FALSE است. بنابراین برای مقایسه دقیق دادههایی که ممکن است NULL داشته باشند، IS DISTINCT FROM انتخاب مناسبی است.۸. اصلاح تمام Referenceهای کدبعد از اینکه دادهها با موفقیت منتقل شدند، باید تمام قسمتهای پروژه را که هنوز به مدل قدیمی اشاره میکنند اصلاح کنیم. فقط دنبال this نباشیم from account.models import Consultation بلکه باید موارد زیر را هم بررسی کنیم: views.py، forms.py، admin.py، services، utils، Generic Views، Generic Services، Configها، Registryها، String referenceها، apps.get_model() و هر جایی که نام Model به صورت String ذخیره شده.یک خطای واقعی که در همین پروژه اتفاق افتادبعد از Deploy روی Live با این خطا مواجه شدیم:LookupError at /account/dashboard/editor/karnosanat/
App 'account' doesn't have a 'Consultation' model.Traceback نشان داد در account/views.py این کد وجود داشت: ge = GenericEditor(business.website, table["key"]) و در Configuration مربوط به جدولها هنوز این مقدار وجود داشت: 'model': 'account.Consultation' در حالی که Model دیگر در account وجود نداشت. باید به این تبدیل میشد: 'model': 'core.Consultation'علت خطا چه بود؟در generic_editor.py این کد وجود داشت: self.model = apps.get_model(app_label, model_name). وقتی Configuration این بود (account.Consultation)، Django دنبال این Model میگشت ولی Model را از account حذف کرده بودیم؛ پس LookupError اتفاق افتاد.درس مهم: در انتقال Model فقط Importها را تغییر نده. حتماً کل پروژه را برای نام قدیمی Model جستجو کن؛ مثلاً account.Consultation را در کل پروژه Search کن، همچنین Consultation را نیز جستجو کن. مخصوصاً اگر پروژه دارای سیستمهای Generic یا Configuration-based باشد.۹. حذف Model قدیمیبعد از اینکه Model جدید ساخته شد، دادهها منتقل و بررسی شدند، Sequence تنظیم شد و تمام Referenceها اصلاح شدند، مدل Consultation را از account/models.py حذف کردیم. سپس:python manage.py makemigrations accountMigration زیر ساخته شد: account/migrations/0012_delete_consultation.py که عملیات آن Delete model Consultation بود.قبل از اجرای Migration روی Live (بسیار مهم)قبل از اینکه Migration مخرب را روی Production اجرا کنیم، SQL واقعی آن را بررسی کردیم:python manage.py sqlmigrate account 0012خروجی:BEGIN;
-- Delete model Consultation
DROP TABLE "account_consultation";
COMMIT;این دقیقاً همان چیزی بود که انتظار داشتیم؛ یعنی Migration فقط جدول قدیمی account_consultation را حذف میکرد.چرا sqlmigrate مهم است؟ چون قبل از اجرای Migration روی Live میتوانیم ببینیم Django واقعاً چه SQLای اجرا خواهد کرد، خصوصاً برای Migrationهای خطرناک مثل DeleteModel، RemoveField و AlterField. این یک Safety Check بسیار خوب است. قاعده: قبل از اجرای Migration مخرب روی Production، sqlmigrate را ببین.python manage.py migrate account 0012خروجی موفق: Applying account.0012_delete_consultation... OK. در نتیجه account_consultation حذف شد، اما core_consultation باقی ماند و دادههای آن هم سر جای خود بودند.۱۰. نهاییکردن related_nameدر ابتدای کار برای جلوگیری از Clash داشتیم related_name="core_consultations". بعد از حذف Model قدیمی دیگر نیازی به این نام موقت نبود و آن را به نام اصلی برگرداندیم:website = models.ForeignKey(
Website,
blank=True,
null=True,
on_delete=models.CASCADE,
related_name="consultations",
verbose_name="وب سایت"
)نکته مهم درباره تغییر related_name: بعد از این تغییر نباید فقط فایل models.py را تغییر دهیم و تمام. باید Django را مجبور کنیم تغییر را به Migration تبدیل کند: python manage.py makemigrations core. اگر Django Migration جدیدی برای تغییر related_name ساخت (مثلاً AlterField)، آن Migration را بررسی و Deploy کنیم. نکته مهم: Migration قبلی (core.0005_consultation) را دستکاری نکن؛ برای تغییر جدید، Migration جدید بساز.۱۱. چکلیست نهایی Productionبعد از پایان انتقال این موارد را بررسی کن:Model: core.Consultation وجود داشته باشد.جدول: core_consultation وجود داشته باشد.جدول قدیمی: account_consultation بعد از Migration حذف شده باشد.تعداد داده: SELECT COUNT(*) FROM public.core_consultation; تعداد مورد انتظار را داشته باشد.صحت داده: Query مقایسه FULL OUTER JOIN خروجی نداشته باشد.Sequence: ID بعدی درست تولید شود (در مثال: آخرین ID = 29، ID بعدی = 30).Referenceها: در کل پروژه دیگر چیزی مثل account.Consultation وجود نداشته باشد.Migration: SQL مربوط به Migration حذف فقط عملیات مورد انتظار را انجام دهد.Application: صفحات و بخشهایی که از Consultation استفاده میکنند تست شوند.Production بعد از Deploy: سرویس Django restart شود (اگر معماری پروژه نیاز دارد)، صفحات مرتبط باز شوند، ساخت رکورد جدید تست شود، ویرایش رکورد تست شود، حذف رکورد (در صورت وجود) تست شود و بخشهایی که از related_name استفاده میکنند تست شوند.۱۲. نگهداری و حذف Safety Copyدر مثال ما Safety Copy این جدول بود: public.account_consultation_backup و داخل آن 29 رکورد وجود داشت. نباید بلافاصله بعد از انتقال آن را حذف کرد. بهتر است چند روز در Production نگه داشته شود تا مطمئن شویم همه چیز پایدار است.DROP TABLE public.account_consultation_backup;SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'account_consultation_backup';اگر هیچ نتیجهای برنگردد یعنی Backup table وجود ندارد.یک توصیه مهم: قبل از حذف آخرین Safety Copy، بهتر است یک Backup واقعی و قابل Restore از کل Database داشته باشی. یعنی Safety Copy داخل Database را با Backup واقعی Database یکی ندانیم.چکلیست سریع برای دفعات بعد☐ 1. Safety Copy از جدول قدیمی☐ 2. کنترل COUNT Backup☐ 3. ایجاد Model در App مقصد☐ 4. جلوگیری از clash در related_name در صورت نیاز☐ 5. makemigrations مقصد☐ 6. بررسی Migration☐ 7. migrate مقصد☐ 8. انتقال داده با INSERT ... SELECT☐ 9. مقایسه COUNT مبدأ و مقصد☐ 10. مقایسه دقیق field-by-field☐ 11. Sync کردن Sequence☐ 12. جستجوی تمام Referenceهای Model قدیمی☐ 13. اصلاح importها☐ 14. اصلاح String Referenceها☐ 15. اصلاح Configها☐ 16. اصلاح Generic Service / Viewها☐ 17. حذف Model قدیمی☐ 18. makemigrations App قدیمی☐ 19. اجرای sqlmigrate و بررسی SQL☐ 20. migrate روی Production☐ 21. اصلاح related_name نهایی در صورت نیاز☐ 22. makemigrations جدید برای تغییر related_name☐ 23. Deploy / Restart☐ 24. Smoke Test☐ 25. چند روز نگهداشتن Safety Copy☐ 26. حذف Safety Copy بعد از اطمیناناشتباهات رایجفقط COUNT گرفتن: این کافی نیست (SELECT COUNT(*)). ممکن است هر دو جدول 29 رکورد داشته باشند ولی دادههایشان متفاوت باشد. پس علاوه بر COUNT، مقایسه دقیق انجام بده.فراموشکردن Sequence: اگر IDها را دستی انتقال دادی (INSERT ... SELECT)، Sequence را فراموش نکن؛ در غیر این صورت ممکن است رکورد جدید با ID تکراری ساخته شود.حذف جدول قبل از اصلاح کد: اگر اول Model قدیمی را حذف کنی و بعد دنبال Referenceها بگردی، ممکن است Production با خطاهایی مثل LookupError مواجه شود. اول Referenceها را اصلاح کن، بعد Model قدیمی را حذف کن.فراموشکردن Configها: یکی از خطرناکترین موارد همین است. ممکن است در کد چیزی مثل from account.models import Consultation نداشته باشی ولی جایی نوشته باشی 'model': 'account.Consultation'. پس باید Stringها را هم جستجو کنی.دستکاری Migration قدیمی: مثلاً اگر Migration core.0005_consultation قبلاً اجرا شده و بعداً related_name تغییر کرد، آن Migration را ویرایش نکن؛ Migration جدید بساز: python manage.py makemigrations coreحذف فوری Backup: بعد از انتقال موفق، وسوسه نشو فوراً DROP TABLE را اجرا کنی. چند روز صبر کن، Production را تست کن، بعد Backup را حذف کن.خلاصه کل فرآیندفرآیند کامل انتقال Model به شکل خلاصه:Backup → ساخت Model در App مقصد → makemigrations → migrate → ساخت جدول جدید → انتقال دادهها → مقایسه COUNT → مقایسه دقیق دادهها → Sync Sequence → اصلاح تمام Referenceها → حذف Model قدیمی → makemigrations → بررسی sqlmigrate → migrate → تنظیم related_name نهایی → Migration جدید در صورت نیاز → Deploy / Restart → Smoke Test → چند روز نگهداشتن Backup → حذف Backupنتیجه نهایی مثال مادر پایان انتقال: App قدیمی (account) ← Consultation ❌ حذف شد. App جدید (core) ← Consultation ✅.account_consultation ❌ حذف شدaccount_consultation_backup 🟡 موقتاً نگه داشته شدcore_consultation ✅ جدول اصلی جدیدداده: 29 رکورد و همه رکوردها: مبدأ = مقصد. Sequence: MAX(id) = 29، ID بعدی = 30. و در نهایت Consultation از account به core با موفقیت، بدون از دست رفتن دادهها و با حفظ IDها منتقل شد. 🚀فرمول طلایی: Backup → Create → Migrate → Copy → Verify → Sync → Refactor → Delete → Testاین را واقعاً میتوان بهعنوان دستورالعمل ثابت انتقال Model در پروژههای Django نگه داری.
بیشتر بخوانید ←