How to correctly refactor Django project: moving models

Photo by Chris Ried on Unsplash

Code Refactoring is a very important step of software development, it makes code easier to read and easier to extend your code, thus it saves you hours of debugging.

source:https://blog.gamewisp.com/dev-update-videos-and-series-410e0a897777

In django applications you define your database schema using models, models defines your applications database structure for database and for django.sometimes you may realise that you designed your database badly(everyone face this problem), in this case you need to move your models between apps like what I did a few days ago.

when I faced this problem I could not find any useful articles and stackoverflow answers so when I figured out how to do it I thought it would be a good idea to write a tutorial for it, so let’s see How to move a django model!.

In my case(which I guess is the most complex case) I had this structure(with removed unrelated fields)

#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)

What I wanted to do was moving ParentModel from app2 to app1, Here are The main steps of Moving a model:

  1. Renaming table of the ParentModel to destination app, since django keeps app1 models table names like this ‘app1_childmodel’ and app2 models like this ‘app2_parentmodel’ so we need to rename our table to fit the new structure
  2. Create migration to moving the ParentModel to app1
  3. update any foreinkeys and relations
  4. Create migration to delete ParentModel from app2
  5. moving the actual code

Renaming Table

To rename the table to destination app you can simply add the meta.db_table to your model, like this

#app2
ParentModel(models.Model):
#fields
class Meta:
db_table = "app1_parentmodel"

Moving The ParrentModel to app1

this is kind of tricky part now what we need to do is to write to functions for moving model and use RunPython command to move ParentModel to app1, But the problem here is We get an error with this migration since ChildModel inherited from ParentModel and in this operation it just loses it’s parent so it leads to this:

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)

What I realized was if I remove ChildModel from the state(I’ll explain what is it) for a few migrations and put it back again I won’t get this error but there was another problem; My OtherModel has a foreignkey to ChildModel so it gives another error. As you may guess, Yes I removed that field from the state too. Now let’s see what do I mean from the word state.

Django models keeps track of your schema for you and for the database, so when django migrates it migrate for both database and your apps database schema state and you can control if some migration should be applied to database or not by using SeprateDatabaseAndState.

here is how to remove ChildModel and that Foreignkey from the 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)
]

In this way we won’t lose our model and it’s data it just goes away from django.

Now we can write migration for moving ParentModel to 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
),
]

You need to specify your model just like the class of the model in CreateModel to avoid any conflicts.

updating foreinkeys and relations

in my case I did not have any foreignkeys pointing to ParentModel but you might have so I’ll show you how to handle it:

Create migrations for any app that contains model with a Foreignkey to you ParentModel and use AlterTable on the field exmaple migration:

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)
]

Use this method to update those relations then you are ready to delete ParentModel from app2

Create migration to delete ParentModel from app2

Now we will try to remove the Parent Model from the state of our app to let django knows it’s not there anymore and since we created it two steps earlier django knows where it is.

class Migration(migrations.Migration):
dependencies = [
('app2', '0001_renaming_table'),
]
state_operations = [
migrations.DeleteModel('ParentModel'),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]

That’s it your good to move to next step

moving the actual code

Move the ParentModel class to app1 and update any references to parerntmodel(eg. imports)

Adding back what we removed from state

Now we need to create migration to recreate ChildModel and the field we removed

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)
]

And.. we are done! I used this method on SQlite and Psql(peoduction) databases it worked perfectly without any data loss.

Hope it was helpful to you, if you had any problems contact me or use comments section