ریفکتور کردن پروژه جنگو: جابجایی مدل ها

این نوشته رو روی مدیوم منتشر کرده بودم حالا تصمیم گرفتم روی ویرگول هم بنویسم.Photo by Chris Ried on Unsplash ریفکتور کردن کد بخش مهمی از توسعه ی نرم افزار هست که باعث میشه کد ما ساده تر و قابل فهم تر بشه و توسعه ی دوباره ی اون رو راحت تر کنه. https://cdn-images-1.medium.com/max/800/1*ZXixptvL4rzkx3EDuj38xw.jpeg این موضوعی بود که چند وقت پیش باهاش روبرو شدم، منابع خیلی مناسبی واسه ی این راه حل پیدا نکردم و حتی توی استک اورفلو هم جواب خوبی پیدا نکردم(!) واسه ی مشکلی که داشتم. به خاطر همین بعد از حل کردن مشکل تصمیم گرفتم درباره ی راه حل این مسئله بنویسم. بریم سراغ جابجایی مدل ها!توی مشکل من همچین مدل هایی وجود داشتن:#app1 class ChildModel(ParentModel): title = models.CharField(max_length=20)class OtherModel(models.Model): relation = models.Foreinkey(ChildModel, on_delete=models.SET_NULL)#app2 class ParentModel(models.Model): name = models.CharField(max_length=20) کاری که قرار بود انجام بشه جابجایی ParrentModel از app2 به app1 بود، روند اصلی این کار اینطوریه:۱- تغییر اسم تیبل ParentModel به اپ مقصد، چون جنگو اسم تیبل هارو به این شکل 'app1_childmodel' نگه میداره پس وقتی اپ یه مدل عوض میشه باید اسم تیبل رو هم تغییر بدیم تا با ساختار جدید همخوانی داشته باشه۲- نوشتن migration برای جابجایی ParentModel به app1۳- اپدیت کردن foreignkeys و رابطه ها۴- نوشتن migration برای پاک کردن ParentModel از app2۵- جابجا کردن کد مدلتغییر اسم تیبلبرای این کار باید از meta.db_table استفاده کنیم، به این صورت:#app2 ParentModel(models.Model): #fields class Meta: db_table = "app1_parentmodel"جابجا کردن ParrentModel به app1این اون قسمتیه که یکمی تریکیه، باید تابعی بنویسیم که اونو با کامند RunPython اجرا کنیم و ParentModel رو به app1 ببره ولی مشکل اینجاست که چون ChildModel به ParentModel وابسته هست به خاطر ارث بری توی این مایگریشن اطلاعات کلاس پدر رو از دست میده و به این اررور میرسیم:django.db.migrations.state.InvalidBasesError: Cannot resolve bases for [<ModelState: 'app1.ChildModel'>] This can happen if you are inheriting models from an app with migrations (e.g. contrib.auth)-چیزی که فهمیدم این بود که اگر ChildModel رو از state اپ پاک کنیم و این تغییرات رو انجام بدیم بعد دوباره ChildModel رو برگردونیم بدون هیچ اروری migration ها اجرا میشن، الان میشه فهمید که پس اون foreignkey هم به همین مشکل میخوره چون که وقتی ChildModel حذف بشه(موقت)‌اون هم رفرنس رو از دست میده و احتمالاً حدس میزنید که اون فیلد رو هم از state پاک میکنم، حالا بریم ببینیم state چیه؟!مدل های جنگو ساختار دیتابیس رو هم برای جنگو تعریف میکنه هم برای دیتابیس و به شما این اجازه رو میده که شما تو هر migration مشخص کنید که آیا این تغییرات باید روی دیتابیس هم انجام بشه یا نه این کارو با استفاده از SeprateDataBaseAndState انجام میدیماین روشی هست که توی اون ChildModel و Foreignkey رو از State پاک میکنیم.#app1 0001 migration from django.db import migrationsclass Migration(migrations.Migration):dependencies = [ ('app2','0001_renaming_the_table'), ]state_operations = [ migrations.RemoveField( model_name='othermodel', name='relation', ), migrations.DeleteModel('ChildModel'), ]operations = [ migrations.SeparateDatabaseAndState(state_operations=state_operations) ]توی این migration هیچ دیتایی از دست نمیره اما از نظر جنگو این مدل و فیلد پاک شدن.حالا میتونیم migration رو برای جابجا کردن ParentModel به app1 بنویسیمfrom django.db import migrations, modelsdef update_contentypes(apps, schema_editor): """ Updates content types. We want to have the same content type id, when the model is moved. """ ContentType = apps.get_model('contenttypes', 'ContentType') db_alias = schema_editor.connection.alias# Move the ParentModel to app1 qs = ContentType.objects.using(db_alias).filter(app_label='app2', model='parentmodel') qs.update(app_label='app1')def update_contentypes_reverse(apps, schema_editor): """ Reverts changes in content types. """ ContentType = apps.get_model('contenttypes', 'ContentType') db_alias = schema_editor.connection.alias# Move the TrackingAlert model to tracking qs = ContentType.objects.using(db_alias).filter(app_label='app1', model='parentmodel') qs.update(app_label='app2')class Migration(migrations.Migration):dependencies = [ ('app1', '0001_delete_from_state'), ('app2', '0001_renaming_table'), ]state_operations = [ migrations.CreateModel( name='ParentModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), #all the other fields ], ), ]database_operations = [ migrations.RunPython(update_contentypes, update_contentypes_reverse), ]operations = [ migrations.SeparateDatabaseAndState( state_operations=state_operations, database_operations=database_operations ), ] توی متد CreateModel حتماً باید مدل رو همونطوری که توی کد هست تعریف کنیم تا ناهماهنگی پیش نیاد.اپدیت کردن ForeignKey ها و روابطتوی این حالتی که من بررسی کردم Foreignkey ای نداشتیم که به ParentModel اشاره کنه، ولی شما ممکنه داشته باشید پس راهشو توضیح میدم.باید برای هر مدلی که دارای این فیلد ForeignKey به مدل ParentModel باشه یه مایگریشن درست کنیم و توی اون با AlterTable مکانی که ParentModel توش هست رو عوض کنیم مثل این:from django.db import migrations, modelsclass Migration(migrations.Migration):dependencies = [ ('app1', '0002_move_parent_model'), # other dependencies ]state_operations = [ migrations.AlterField( model_name='somemodel', name='theforeignkeyfield', field=models.ForeignKey(on_delete=models.deletion.CASCADE, to='app1.ParentModel'), ), ]operations = [ migrations.SeparateDatabaseAndState(state_operations=state_operations) ]بعد از اپدیت کردن ریلیشن ها آماده ایم که کد مدل رو جابجا کنیم(yay!)نوشتن مایگریشن برای پاک کردن مدل ParentModel از app2چرا این مرحله باید انجام بشه؟ چون گفتم که جنگو یه اطلاعات از ساختار دیتابیس شما داره که اون رو اعمال میکنه روی دیتابیس ما نمیخوایم که مدل توی دیتابیس تغییر بکنه(پاک بشه) ولی میخوایم جنگو فکر کنه این مدل از این اپ پاک شده و به app2 رفته پس این مایگریشن رو مینویسم:class Migration(migrations.Migration):dependencies = [ ('app2', '0001_renaming_table'), ]state_operations = [ migrations.DeleteModel('ParentModel'), ]operations = [ migrations.SeparateDatabaseAndState(state_operations=state_operations) ]این مرحله هم تموم شد میریم مرحله بعدیجابجا کردن کد مدلحالا کد مدل رو میبرم توی اون اپ و import هایی که داریم رو هم درست میکنیم.بعد از این میتونیم اون فیلد و مدلی که اول کار یه جورایی به جنگو گفتیم که وجود ندارند رو دوباره برگردونیم که کار سختی نیست با CreateModel و Addfield و SeparateDatabaseAndState انجامش میدیم:from django.db import migrations, models import djangoclass Migration(migrations.Migration):dependencies = [ ('app1', '0002_move_parent_model'), ]state_operations = [ migrations.CreateModel( name='ChildModel', fields=[ ('parentmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='app1.ParentModel')), ('title',models.CharField(max_length=20,), ] bases=('app1.parentmodel',)) migrations.AddField( model_name='othermodel', name='relation', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_Null), to='app1.ChildModel') ),operations = [ migrations.SeparateDatabaseAndState(state_operations=state_operations) ]و.. تموم شد. من این روش رو روی دیتابیس های SQlite و Psql تست کردم و به خوبی جواب داده اگر باز هم به مشکلی خوردید میتونید با من در ارتباط باشید.امیدوارم مفید بوده باشه.