Wednesday, 25 May 2022

Django alter existing migration including add not null column

To elaborate on my comment above...

Adding a new non-nullable ForeignKey in Django is generally a three-step process.

  1. First, you add the new ForeignKey to your model definition with null=True, and run makemigrations. This will create a migration that will add the field, nothing special about it. Executing this migration will add a column with all rows having NULL as the value.
  2. Second, you create a new empty migration for the same app (makemigrations --empty), then edit that migration to contain a data migration step. This is where you'll need to, according to your business logic, choose some value for the new foreign key.
  3. Third, you modify the ForeignKey in your model definition to set null=False and create a third migration with makemigrations. Django will ask whether you've dealt with nulls somehow – you need to say that "yep, I swear I have" (since you did, above in step 2).

In practice, for a simplified version of OP's question where we'll want to add an User foreign key:

Original state

class Post(models.Model):
    name = models.CharField(max_length=100)

1a. Add nullable field.

class Post(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(User, null=True, on_delete=models.CASCADE))

1b. Run makemigrations.

$ python makemigrations
Migrations for 'something':
    - Add field author to post

2a. Create a new empty migration.

$ python makemigrations something --empty -n assign_author
Migrations for 'something':

2b. Edit the migration file.

More information on data migrations can be found, as always, in the manual.

from django.db import migrations

def assign_author(apps, schema_editor):
    User = apps.get_model('auth', 'User')  # or whatever is your User model
    Post = apps.get_model('something', 'Post')  # or wherever your Post model is
    user = User.objects.filter(is_superuser=True).first()  # Choose some user...
    assert user  # ... and ensure it exists...
    Post.objects.all().update(author=user)  # and bulk update all posts.

class Migration(migrations.Migration):

    dependencies = [...]

    operations = [
        migrations.RunPython(assign_author, migrations.RunPython.noop),

3a. Make the field non-nullable.

class Post(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(User, null=False, on_delete=models.CASCADE))

3b. Run Makemigrations.

Answer truthfully to the question – you've just added a RunPython operation.

$ python makemigrations something -n post_author_non_null
You are trying to change the nullable field 'author' on something. to non-nullable without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL operation to handle NULL values in a previous data migration)
 3) Quit, and let me add a default in
Select an option: 2
Migrations for 'something':
    - Alter field author on post

All done!

Running migrate will now run these three migrations and your model will have author without data loss.

No comments:

Post a Comment