In Django, I am trying to move a custom User model from one app to another. When I follow instructions such as found here, and try to apply the migration, I get errors with ValueError: Related model 'newapp.user' cannot be resolved
that originate from django.contrib.admin.models.LogEntry.user
, which is defined as models.ForeignKey(settings.AUTH_USER_MODEL, ...)
, so that is a model from the Django admin (which I also use) that has a foreign key to the User model. How can I do this migration?
For swappable models (models that can be swapped out through a value in settings
) such a move is non-trivial. The root cause of the problem here is that LogEntry
has a foreign key to settings.AUTH_USER_MODEL
, but the history of settings.AUTH_USER_MODEL
itself is not managed or known to Django migrations. When you change AUTH_USER_MODEL
to point to the new User model, that retroactively changes the migration history as Django sees it. To Django it now looks like LogEntry's foreign key has always referenced the new User model in destapp. When you run the migration that creates the table for LogEntry (e.g. when re-initializing the database or running tests), Django cannot resolve the model and fails.
See also this issue and the comments there.
To work around this problem, AUTH_USER_MODEL
needs to point to a model that exists in the initial migration for the new app. There are a few approaches to get this working. I'll assume the User
model is moved from sourceapp
to destapp
.
Move the migration definition for the new User model from the last migration (where Django makemigrations would automatically put it) to the initial migration of destapp. Leave it wrapped in a
SeparateDatabaseAndState
state operation, because the database table is already created by the migrations in the sourceapp. Then, you'll need to add a dependency from the initial migration of destapp to the last migration of sourceapp. The problem is that if you try to apply the migrations as they are now, it will fail because destapp's initial migration has already been applied while it's dependency (sourceapp's last migration) has not been. So you will need to apply the migrations in sourceapp before adding the above migrations in destapp. In the gap between applying sourceapp and destapp's migrations the User model won't exist so your application will be temporarily broken.Apart from temporarily breaking the application, this has the other problem that now destapp will depend on sourceapp's migrations. If you can do that, that's fine, but if there already exists a dependency from a sourceapp migration to a destapp migration this won't work and you've now created a circular dependency. If that's the case, look at the next options.
Forget about the User migration history. Just define the User class in destapp's initial migration, without the
SeparateDatabaseAndState
wrapper. Make sure you haveCreateModel(..., options={'db_table': 'sourceapp_user'}, ...)
, so the database table will be created the same as it would when User lived in sourceapp. Then edit sourceapp's migration(s) where User is defined, and remove those definitions. After that, you can create a regular migration where you remove User'sdb_table
setting so the database table gets renamed to what it should be for destapp.This only works if there are no or minimal migrations in the migration history of sourceapp.User. Django now thinks User always lived in destapp, but its table was named as
sourceapp_user
. Django cannot track any database-level changes tosourceapp_user
anymore since that information was removed.If this works for you, you can either forego any dependencies between sourceapp and destapp, if sourceapp's migrations don't need User to be there, or have sourceapp's initial migration depend on destapp's initial migration so that the table for User is created before sourceapp's migrations are run.
If both don't work in your situation, another option is to add the definition for User to sourceapp's initial migration (without
SeparateDatabaseAndState
wrapper), but have it use a dummy table name (options={'db_table': 'destapp_dummy_user'}
). Then, in the newest migration where you actually want to move User from sourceapp to destapp, domigrations.SeparateDatabaseAndState(database_operations=[ migrations.DeleteModel( name='User', ), ], state_operations=[ migrations.AlterModelTable('User', 'destapp_user'), ])
This will delete the dummy table in the database, and point the User model to the new table. The new migration in sourceapp should then contain
migrations.SeparateDatabaseAndState(state_operations=[ migrations.DeleteModel( name='User', ), ], database_operations=[ migrations.AlterModelTable('User', 'destapp_user'), ])
so it is effectively the mirror image of the operation in the last destapp migration. Now only the last migration in destapp needs to depend on the last migration in sourceapp.
This approach appears to work, but it has one big disadvantage. The deletion of destapp.User's dummy database table also deletes all foreign key constraints to that table (at least on Postgres). So LogEntry now no longer has a foreign key constraint to User. The new table for User doesn't recreate those. You will have to add the missing constraints back in again manually. Either by manually updating the database or by writing a raw sql migration.
Update content type
After applying one of the three above options there's still one loose end. Django registers every model in the django_content_type
table. That table contains a line for sourceapp.User
. Without intervention that line will stay there as a stale row. That isn't a big problem as Django will automatically register the new destapp.User
model. But it can be cleaned up by adding the following migration to rename the existing content type registration to destapp:
from django.db import migrations
# If User previously existed in sourceapp, we want to switch the content type object. If not, this will do nothing.
def change_user_type(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
ContentType.objects.filter(app_label="sourceapp", model="user").update(
app_label="destapp"
)
class Migration(migrations.Migration):
dependencies = [
("destapp", "00xx_previous_migration_here"),
]
operations = [
# No need to do anything on reversal
migrations.RunPython(change_user_type, reverse_code=lambda a, s: None),
]
This function only works if there is no entry in django_content_type
for destapp.User
yet. If there is, you'll need a smarter function:
from django.db import migrations, IntegrityError
from django.db.transaction import atomic
def change_user_type(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
ct = ContentType.objects.get(app_label="sourceapp", model="user")
with atomic():
try:
ct.app_label="destapp"
ct.save()
return
except IntegrityError:
pass
ct.delete()
© 2022 - 2024 — McMap. All rights reserved.