[Django]-How to modify a models who's already migrated in Database?

7đź‘Ť

âś…

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 manage.py makemigrations
Migrations for 'something':
  something/migrations/0002_post_author.py
    - Add field author to post

2a. Create a new empty migration.

$ python manage.py makemigrations something --empty -n assign_author
Migrations for 'something':
  something/migrations/0003_assign_author.py

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 manage.py 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 models.py
Select an option: 2
Migrations for 'something':
  something/migrations/0004_post_author_non_null.py
    - Alter field author on post

All done!

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

👤AKX

1đź‘Ť

First you need to edit your models.py

# models.py
class Post(models.Model):
    name = models.CharField(max_length=100)
    email = models.CharField(max_length=100)
    gender = models.CharField(max_length=8, choices=THE_GENDER)
    number = models.CharField(max_length=100)
    author = models.ForeignKey(User, on_delete=models.CASCADE) # add new field

    def __str__(self):
        return self.name

Since you added new field author, you have to run makemigrations command and then the migrate command.
During migrations, you may be asked to provide a default value for author field for existing Post instances. In that time, you can “interactively” provide values via Django shell. Any valid PK of User instances will be a valid input during the time. FYI: This is a onetime process

After that, change your home(...) view as,

from django.contrib.auth.decorators import login_required


@login_required
def home(request):
    form = post_form(request.POST or None)
    if form.is_valid():
        instance = form.save(commit=False)
        instance.auther = request.user
        instance.save()

    context = {
        "form": form
    }

    return render(request, "sms/home.html", context)

Note: You should use @login_required(...) decorator to get the authenticated user in request.user attribute.

👤JPG

0đź‘Ť

Your migration is tricky, because you want every Post related to a user. To workaround this you can do the following:

Add your new field with null=True
models.ForeignKey(User, on_delete=models.CASCADE, null=True)
then manage.py makemigrations
then you have to decide what to do. Either (1) add authors to your existing posts by hand, or (2) delete them all

manage.py shell
from x.models import Post
from django.contrib.auth.models import User

# 1
user = User.objects.get(id=1) # get your user
Post.objects.all().update(author=user)

# or 2
Post.objects.all().delete()

then you can finally set your authors like you want them:
models.ForeignKey(User, on_delete=models.CASCADE)
again manage.py makemigrations

See the comments for more information

👤ChrisRob

0đź‘Ť

You can handle this issue in different ways.

If there is no data, you can easily add the new field and run your migrations. You should be fine

If you already have posts, you can allow the field to be nullable, then run your migrations before going to populate it.

A more intuitive way of doing this especially if you already have data is to assign a user to the post while running the migrations.

First create a migration with the author field allowed to be null, then

author = models.ForeignKey(User, null=True, on_delete=models.CASCADE)

manage.py makemigrations
manage.py migrate

after that, remove the null attribute.

author = models.ForeignKey(User, on_delete=models.CASCADE)

manage.py makemigrations

open the migration file now generated which will be which you will modify with a custom method like below and run migrate after that.

from django.db import migrations, models


# create custom method
def add_author(apps, schema_editor):
    User = apps.get_model('auth', 'User')  
    Post = apps.get_model('something', 'Post')  
    posts = Post.objects.all()
    author = User.object.get(id=1) # whichever user you want to use as the author
    for post in posts:
        post.author = author
        post.save()

class Migration(migrations.Migration):

    dependencies = [
        ('posts', '00000000'),
    ]

    operations = [
        migrations.RunPython(add_author), # this will run the method above
        migrations.AlterField(
            model_name='post',
            name='author',
            field=models.ForeignKeyField...,
        ),

    ]

check this link for a more detailed explanation of how this works

Leave a comment