راهنمای کامل انتقال یک 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.
تنظیم Sequence
SELECT 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 account
Migration زیر ساخته شد: 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 نگه داری.
