Opened 2 years ago

Last modified 9 months ago

#25313 new New feature

Document how to migrate from a built-in User model to a custom User model

Reported by: Carl Meyer Owned by: nobody
Component: Documentation Version: 1.8
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Tim Graham)

So far our answer here has been "sorry, you can't do it, unless you're very familiar with the depths of the migrations system and willing to put a lot of time into it."

I don't believe that this is a tenable or defensible answer. It puts too many of our users, too frequently, into an impossible quandary. I think we need to clearly document how you can do it today, even if the process is nasty and ugly. Hopefully seeing that nasty and ugly documentation might clarify what could be improved to make the process less nasty and ugly.

Change History (8)

comment:1 Changed 2 years ago by Tim Graham

Component: MigrationsDocumentation
Description: modified (diff)
Triage Stage: UnreviewedAccepted

comment:2 Changed 2 years ago by Aymeric Augustin

I did it at least twice. Unfortunately I don't remember all the details.

I think a reasonable procedure is:

  1. Create a custom user model identical to auth.User, call it User (so many-to-many tables keep the same name) and set db_table='auth_user' (so it uses the same table)
  2. Throw away all your migrations
  3. Recreate a fresh set of migrations
  4. Sacrifice a chicken, perhaps two if you're anxious; also make a backup of your database
  5. Truncate the django_migrations table
  6. Fake-apply the new set of migrations
  7. Unset db_table, make other changes to the custom model, generate migrations, apply them

It is highly recommended to do this on a database that enforces foreign key constraints. Don't try this on SQLite on your laptop and expect it to work on Postgres on the servers!

comment:3 Changed 2 years ago by Carl Meyer

Hmm. I thought I recalled you mentioning (at DUTH last year?) that you achieved it using SeparateDatabaseAndState, without the need to wipe migrations and start over. Maybe that was someone else.

comment:4 Changed 2 years ago by Aymeric Augustin

At some point I thought it was doable with SeparateDatabaseAndState but eventually I realized that it isn't.

When you change settings.AUTH_USER_MODEL, suddenly Django has a different view of the migration history. Piling hacks cannot hide this fact. I don't think it's possible to salvage migration history when changing AUTH_USER_MODEL.

comment:5 Changed 2 years ago by Carl Meyer

Hmm. Clearly it must be partially possible, otherwise you could never do this at all if you use any third-party apps that link to User (since you won't be changing their migration files). The whole reason we special-case swappable models in migrations (instead of just treating them concretely) is to allow for migrations to not be dependent on the value of AUTH_USER_MODEL, so that reusable apps depending on User can still generate workable migrations.

It's true that changing AUTH_USER_MODEL changes the _meaning_ of historical migrations in some sense, but it still seems to me that if our approach for reusable apps actually works, the same migration files ought to be salvageable (presuming that when you switch AUTH_USER_MODEL you point it to a new model that is initially exactly the same as the previous one, and then only modify it in later, separate migrations).

But you've actually done this and I haven't, so I'm probably wrong...

comment:6 in reply to:  5 Changed 2 years ago by Aymeric Augustin

Replying to carljm:

Hmm. Clearly it must be partially possible, otherwise you could never do this at all if you use any third-party apps that link to User (since you won't be changing their migration files).

Indeed you don't change the migration files. But you drop django_migrations, change settings.AUTH_USER_MODEL and repopulate django_migrations. While some migrations have the same name in django_migrations, you don't have the same set of migrations and their semantic has changed. Specifically:

  • you add at least the migration that creates your custom user model
  • the auth.000_initial migration has a different semantic because it doesn't create a table for auth.User

The whole reason we special-case swappable models in migrations (instead of just treating them concretely) is to allow for migrations to not be dependent on the value of AUTH_USER_MODEL, so that reusable apps depending on User can still generate workable migrations.

That's the trick. A migration file that contains a migration that uses the swappable option isn't a self-contained definition. Only the combination of the migration file and the value of getattr(django.conf.settings, swappable) is. This isn't reflected in the structure of django_migrations because Django currently assumes AUTH_USER_MODEL to be immutable.

It's true that changing AUTH_USER_MODEL changes the _meaning_ of historical migrations in some sense, but it still seems to me that if our approach for reusable apps actually works, the same migration files ought to be salvageable (presuming that when you switch AUTH_USER_MODEL you point it to a new model that is initially exactly the same as the previous one, and then only modify it in later, separate migrations).

I remember feeling smart, then making a huge mess of a SQLite database, feeling dumb, editing the dump manually to fix broken FK constaints, feeling lucky.

But you've actually done this and I haven't, so I'm probably wrong...

Well, perhaps there's a way.

Even then I'd recommend the procedure I suggested above because:

  • it's reasonably convenient: people have a fair chance to execute it successfully
  • it makes it clear that you're voiding your warranty (not that Django comes with a warranty, but you get the point)
  • it's possible to reason about why it works

comment:7 Changed 2 years ago by Shai Berger

<brainstorming idea="halfbaked">

Perhaps it is possible to create a migration operation for changing a swappable model.

Something like:

ChangeSwappableModel(
    setting="AUTH_USER_MODEL",
    old="auth.User",
    new="my_auth.User"
)

This would be a migration in the my_auth app.

It would need to introspect the database, checking the constraints corresponding to my_auth.User's reverse relationships, and verifying that they indeed point to the correct table (if they point to auth_user, change them; if to my_auth_user, leave them be; otherwise, error out).

To change the swappable model, one would:
1) Create the set of migrations for creating the new user model, porting existing data to it, and the ChangeSwappableModel operation;
2) Create a "squashing" migration replacing them by just creating the new model, so new databases don't have to suffer

I think with careful migrations embargos in the right times this could be made to work. I don't have it in me to work out all the details now, and I'm probably missing something essential.

</brainstorming>

comment:8 Changed 9 months ago by James Addison

I think I've followed a very similar process in previous projects (older Django versions) to what Aymeric mentions above in https://code.djangoproject.com/ticket/25313#comment:2 - although I don't think you can ever 'unset' db_table without having to do low level SQL changes?

Note: See TracTickets for help on using tickets.
Back to Top