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

این نوشته رو روی مدیوم منتشر کرده بودم حالا تصمیم گرفتم روی ویرگول هم بنویسم.

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 []
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 migrations
class 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, models
def 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, models
class 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 django
class 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 تست کردم و به خوبی جواب داده اگر باز هم به مشکلی خوردید میتونید با من در ارتباط باشید.

امیدوارم مفید بوده باشه.

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

گردآوری توسط ایده طلایی

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *