"Allow moving a model between apps" proposal for Google Summer of Code 2023.
- Abstract
- 1.1 Overview
- 1.2 Goals
- 1.3 Benefits
- The solution
- 2.1 Modified
CreateModeloperation . - 2.2 Modified
AlterModelTableoperation . - 2.3 Modified
DeleteModeloperation . - 2.4 New
generate_move_models()method in autodetector.
- 2.1 Modified
- Schedule and milestones
- About me
In long running projects , there often arrives a situation where one or more model(s) are required to move from one app to the other. For example,
Imagine you have a Django project for a blogging platform, and you have two apps: posts and users. Initially, you put all the models related to posts in the posts app, including the Post model, the Comment model, and the Tag model. However, as your project grows, you realize that you need to add some features related to user profiles, such as user avatars, user bios, and social media links.
In this case, you might decide to create a new app called profiles and move the UserProfile model from the users app to the new profiles app. However, the UserProfile model has a foreign key to the Post model, which means you need to update the Post model to use the new UserProfile model without losing any data or breaking any existing functionality.
Before:
- posts
- Post
- Comment
- Tag
- users
- BaseUser
- UserProfile
After:
- posts
- Post
- Comment
- Tag
- users
- BaseUser
- profiles
- UserProfile
One of the solution is to use custom RunRython operation.The Django docs have some information on this which uses RunPython operation and bulk_create to insert data in newly created model in new app, but this process would take a lot of time when you have a huge database, leading to increased downtime of your application in production.
There is another solution posted on SO which uses the powerful SeparateDatabaseAndState operation to move model between the apps and is better than the latter. But even this process requires a lot of manual work which slows down the developement of an application.
There is a long standing ticket to have migration operations auto-generated which would definitely save a lot of time of developers and improve maintainability.
This proposal is about modifying the existing CreateModel , DeleteModel and AlterModelTable operations so that they can work on database conditionally and auto-detect the movement of model(s).
- The first milestone would be to modify
DeleteModelandAlterModelTablemigration operation which will be placed in theold app,CreateModelplaced innew_appalong with tests and documentation. - The second milestone would be to write
generate_moved_modelsmethod for detecting a moved model ,writing tests and better documenting the working and use ofSeparateDatabaseAndStateand guide on moving a model usingSeparateDatabaseAndState.
- Models will be easily moved to any app without any data loss or code break.
- Supports reverse migration in case wrong model is moved.
- Will detect the moved model and auto generate migration for you.
- Turn off old app after moving model to new app with very less manual work.
- Closes ticket #24686
First of all we will create a flag state_only_op(or maybe some different name) in model options which will allow DeleteModel and CreateModel to work conditionally on database. Means if state_only_op is set to True for DeleteModel or CreateModel, then they will only change the state of the model and not the database. This approach provides control over every step involved in moving a model.
Now the whole process of moving a model will basically involve 3 steps in 3 different migration files:
- Creating a new migration file in old app and add
AlterModelTableoperation in it to change table name to new one along with setting thestate_only_optoTruefor the model to be moved. - Then creating a new migration file in new app and adding
CreateModeloperation which will create a new model state for moved model in new app. In this model state also we'll setstate_only_optoTrueso that the model is created in state only(as we already have the table).AfterCreateModeloperationAlterFieldoperation will be added (in their own apps) for all the fields(if any) which were pointing to the moved model. - Another migration file will be added in old app with
Alterfieldoperation for fields referencing the moved model (if any) in old app and in the endDeleteModeloperation will be added to delete state of old model from old app. Now thisDeleteModelwill be applied only on state due to thestate_only_opflag which was set toTrueduringAlterModelTableoperation.
The CreateModel operation will be placed in the app to which the model is being moved.
- First we are going to add
state_only_optoCreateModelas an keyword argument and then initialize it to the value passed(eitherTrueorFalse).
...
def __init__(
self, name,
fields, options=None,
bases=None,
managers=None,
state_only_op=None
):
self.fields = fields
self.state_only_op = state_only_op or False
...
- Then we'll create a function say
update_op_typein ProjectState to update the flagstate_only_opin state.
...
def update_op_type(self, app_label, model_name, options):
model_state = self.models[app_label, model_name]
model_state.options = {**model_state.options, **options}
self.reload_model(app_label, model_name, delay=True)
...
- Now in
state_forwardsofCreateModelwe'll callupdate_op_typemethod after adding model to state.
...
state.update_op_type(app_label, self.name_lower, {
"state_only_op": self.state_only_op})
...
- Then in
database_forwardsanddatabase_backwardswe'll check ifstate_only_opis not true, then only we'll perform operation on database.
...
if not model._meta.state_only_op:
if self.allow_migrate_model(schema_editor.connection.alias, model):
schema_editor.create_model(model)
...
The AlterModelTable will be placed in the old_app to remove old model from state.
- Similar to
CreateModelwe'll addstate_only_opas a keyword argument and initialize it to the value passed(eitherTrueorFalse).
...
def __init__(self, name, table, state_only_op=None):
self.table = table
self.state_only_op = state_only_op or False
...
- Then in
state_forwardswe'll callupdate_op_typemethod to update the flag of old state of moved model.
...
def state_forwards(self, app_label, state):
state.update_op_type(app_label, self.name_lower, {
"state_only_op": self.state_only_op})
state.alter_model_options(app_label, self.name_lower, {"db_table": self.table})
...
Yes ,updating state_only_op in AlterModelTable seems to be a bit confusing, but it is the only mandatory operation before DeleteModel for altering old model state and we cannot update it in DeleteModel.
We can document this behaviour in the process of moving model between apps.
The DeleteModel will be placed in the old_app to remove old model from state.
- Now for
DeleteModelwe'll just check the flagstate_only_opand only perform database operation when its False. This will delete the old model state of moved model.
...
if not model._meta.state_only_op:
if self.allow_migrate_model(schema_editor.connection.alias, model):
schema_editor.delete_model(model)
...
A new method will be written in autodetector.py which will detect the moved model(s) and auto-generate migration operations .
-
First we will find the models added in the new app with the help of
model keysand store them inadded_models.... added_models = self.new_model_keys - self.old_model_keys ... -
Then we will loop through the
added modelsand extract model state and model fields definition. we are also going to find and store the model(s) removed from the old app. All this information will be used to compare if the model which was added in thenew_appis same as it was in theold_appand not a newly created model.Note: Only one model will be detected at a time as it will require multiple operations in multiple files with dependencies.
... for app_label, model_name in sorted(added_models): model_state = self.to_state.models[app_label, model_name] model_fields_def = self.only_relation_agnostic_fields(model_state.fields) removed_models = self.old_model_keys - self.new_model_keys ... -
Then we will create another loop inside the above one to compare
added_modelsandremoved_models.If the removed_model and added_model have different app labels but same model field definitions, it means the model is moved from one app to another. We will confirm it with the help of a new questioneras_move_model().... for moved_app_label, moved_model_name in removed_models: if moved_app_label != app_label: moved_model_state = self.from_state.models[ moved_app_label, moved_model_name ] moved_model_fields_def = self.only_relation_agnostic_fields( moved_model_state.fields ) if model_fields_def == moved_model_fields_def: if self.questioner.ask_move_model( moved_model_state, model_state ): ... -
If the user confirms that a model has been moved, then we will have to construct dependencies for related fields and also to detect the next migrations.
... if model_fields_def == moved_model_fields_def: if self.questioner.ask_move_model( moved_model_state, model_state ): dependencies = [] fields = list(model_state.fields.values()) + [ field.remote_field for relations in self.to_state.relations[ app_label, model_name ].values() for field in relations.values() ] for field in fields: if field.is_relation: dependencies.extend( self._get_dependencies_for_foreign_key( app_label, model_name, field, self.to_state, ) ) ... -
Once dependencies are created, we are going to add different operations in the in app specific migration files starting with
AlterModelTableoperation in old app withstate_only_opset toTrue.
...
self.add_operation(
rem_app_label,
operations.AlterModelTable(
name=model_name,
table=db_table,
state_only_op=True
),
dependencies=dependencies,
)
...
- Then we'll add
CreateModelin new app withstate_only_opset toTrue.
...
self.add_operation(
app_label,
operations.CreateModel(
name=model_state.name,
fields=[
d
for d in model_state.fields.items()
],
options=model_state.options,
bases=model_state.bases,
managers=model_state.managers,
state_only_op=True,
),
dependencies=dependencies,
beginning=True,
)
...
- After
CreateModel, we'll addAlterFieldoperation for each foreign key in their specific app. We have to keep track of all alterfield operations so thatgenerate_altered_fieldsmethod in autodetector don't add more of them and we can ignore them.
...
for field in fields:
if field.is_relation and field.remote_field.related_model != model:
self.add_operation(
field.related_model._meta.model_name,
operations.AlterField(
model_name=field.related_model._meta.model_name,
name=field.remote_field.name,
field=field.remote_field,
),
dependencies=dependencies,
)
self.already_alter_fields.add(
(
field.related_model._meta.model_name,
field.related_model._meta.model_name,
field.remote_field.name
)
)
...
Note: Generic Foreign keys are ignored and AlterField operation will not be added for them because generic foreign key is not actually a field in the database. Instead, it is implemented using two separate fields: a foreign key to the content type model and another foreign key to the specific object instance within that content type. So we'll add Generic Foreign Keys in CreateModel in new app along with other fields so that it can be applied on empty db in case old app is turned off.
- In the end,
DeleteModeloperation will be added in second migration file in old app to remove the old state of moved model. So there will be atleast 3 new migration files created(2 in old app ,1 in new app).
...
self.add_operation(
rem_app_label,
operations.DeleteModel(
name=rem_model_name),
dependencies=dependencies,
)
...
Note: There can be a situation where we need to turn off the old app after moving a model the new app and apply the migrations on an empty database.This can be acheived with very less manual work as follows:
- Remove the old app from
INSTALLED_APPSand old app dependencies from the migration files in new app. - Run
squashmigrationson new app. - Set
state_only_optoFalse(or remove state_only_op) inCreateModeloperation generated after squashing. - Run
migratecommand to apply the changes on empty database.
My final exams would be conducted during june (the final date is not decided yet). Till then I have regular offline classes plus training, still I would be able to devote 30-35 hours a week (2-3 hours on weekdays and 4-6 hours on weekends) throughout the GSoC period ( a little less during final exams).
I would like to devote 50% of my time to learning and coding, and 50% of my time to test the changes and write documentation for new stuff. I will write blog posts every weekend to make the community aware of my progress, contributions, and a plan for next week.
- Discuss approach and implementation with senior developers or Mentors.
- Figure out if there is any better approach to the problem or the solution could be improved in any way.
(From May 29 to July 10)
During this phase, I will work on modifying CreateModel,DeleteModel and AlterModelTbable migration operation in models.py, update_op_type method in ProjectState .
- Creating
state_only_opflag. - Writing new method for updating flag
- Fixing and Writing tests along with Documentaion.(if required)
- Initializing
CreateModelmethod withstate_only_opflag. - Calling
update_only_dbto update flag. - Adding condition for
schemaEditorto work as per flag. - Fixing and Writing tests related to CreateModel along with Documentation.(if required)
- Initializing
CreateModelmethod withstate_only_opflag. - Calling
update_only_dbto update flag forDeleteModelto work as per flag.. - Fixing and Writing tests related to AlterModelTable along with Documentation.(if required)
- Adding condition for
schemaEditorto work as per flag. - Fixing and Writing tests related to CreateModel along with Documentation.(if required)
- Fixing and Writing tests related to modified migration operations.
- Documenting the changes required.
- Review Fixing.
(From July 14 to August 21)
During this time i'll be writing generate_moved_models method in autodetector.py along with writing test related to auto-detector and better documenting SeparateDatabaseAndState for moving models.
- Writing logic for detecting movement of models.
- Creating dependencies and operations for foreign keys.
- Auto-generating operations
AlterModelTable,DeleteModelin old app andCreateModelin new app. - Fixing and Writing tests related to autodetector along with Documentation.(if required)
- Writing tests for moving models between apps for various cases (if required).
- Better documentation is needed along with an example with detailed explanation.
- I would like to work on the issue Adapt schema editors to operate from model states instead of fake rendered models which was a success in GSOC 2021.
My name is Bhuvnesh Sharma and I am a Pre-final year Btech. student from Dr. A.P.J. Abdul Kalam Technical University (India). I started my coding journey when i was in my 11th grade with C++ then shifted to python in about a year. I started learnig Django in my freshman year and created a video calling application during covid.
I started contributing to django in September 2022 and found it really interesting. The community is very also supportive and so I would like to thank all the community members of Django for helping me out in this journey.
4.1.1 Issues Fixed
- #28987 : Migration changing ManyToManyField target to 'self' doesn't work correctly.
- #33975 : __in doesn't clear selected fields on the RHS when QuerySet.alias() is used after annotate().
- #33995 : Rendering empty_form crashes when empty_permitted is passed to form_kwargs.
- #34019 : "Extending Django's default user" section refers to a deleted note.
- #34112 : Add async interface to Model.
- #34137 : model.refresh_from_db() doesn't clear cached generic foreign keys.
- #34171 : QuerySet.bulk_create() crashes on mixed case columns in unique_fields/update_fields.
- #34217 : Migration removing a CheckConstraint results in ProgrammingError using MySQL < 8.0.16.
- #34250 : Duplicate model names in M2M relationship causes RenameModel migration failure.
4.1.2 Pull Requests (Merged)
- PR-16032 : Fixed #33975 -- Fixed __in lookup when rhs is a queryset with annotate() and alias().
- PR-16034 : Refs #33616 -- Updated BaseDatabaseWrapper.run_on_commit comment.
- PR-16041 : Fixed #33995 -- Fixed FormSet.empty_form crash when empty_permitted is passed to form_kwargs.
- PR-16065 : Fixed #34019 -- Removed obsolete references to "model design considerations" note.
- PR-16242 : Fixed #34112 -- Added async-compatible interface to Model methods.
- PR-16251 : Refs #33646 -- Moved tests of QuerySet async interface into async tests.
- PR-16260 : Fixed #34137 -- Made Model.refresh_from_db() clear cached generic relations.
- PR-16281 : Fixed #28987 -- Fixed altering ManyToManyField when changing to self-referential.
- PR-16315 : Fixed #34171 -- Fixed QuerySet.bulk_create() on fields with db_column in unique_fields/update_fields.
- PR-16405 : Fixed #34217 -- Fixed migration crash when removing check constraints on MySQL < 8.0.16.
- PR-16532 : Fixed #34250 -- Fixed renaming model with m2m relation to a model with the same name.
- Email : [email protected]
- Timezone : Indian Standard Time (UTC + 5:30)
- Primary Language : English
- Country of Residence : India
- LinkedIn Profile : Bhuvnesh Sharma