16

Using migrations, I need to add a new field (a foreign key) to a model. I know it can be done with:

    migrations.AddField(
        model_name='MyModel',
        name='state',
        field=models.ForeignKey(null=True, related_name='mymodel_state', to='msqa_common.MyModelState'),
    ),

However, I don't want my field to be nullable. Instead, I want to use a default value for it, corresponding to the id of MyModelState whose name is "available" (id value might change in different machines). This "available" value of table MyModelState is inserted into the database in a previous migration script, so it does exist.

I guess I should do something like:

    migrations.AddField(
        model_name='MyModel',
        name='state',
        field=models.ForeignKey(null=False, default=available_state_id, related_name='mymodel_state', to='msqa_common.MyModelState'),
    ),

My question: How can I get the available_state_id within my migration script?

eelioss
  • 359
  • 1
  • 5
  • 10

3 Answers3

32

You can't do it directly. The recommended way of doing this is to create a migration to add it with null=True, then add a data migration that uses either Python or SQL to update all the existing ones to point to available_state_id, then a third migration that changes it to null=False.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
23

I just had the same issue and stumbled upon this answer, so here is how I did it:

  operations = [
        # We are forced to create the field as non-nullable before
        # assigning each Car to a Brand
        migrations.AddField(
            model_name="car",
            name="brand",
            field=models.ForeignKey(
                null=True,
                on_delete=django.db.models.deletion.PROTECT,
                to="model.Brand",
            ),
        ),

        # assign_car_to_brand loops over all my Car objects and sets their
        # "brand" field
        migrations.RunPython(add_category_to_tags, do_nothing),

        # Make the field non-nullable to force all future Car to have a Brand
        migrations.AlterField(
            model_name="car",
            name="brand",
            field=models.ForeignKey(
                null=False,
                on_delete=django.db.models.deletion.PROTECT,
                to="model.Brand",
            ),
            preserve_default=False
        ),

    ]
Be Chiller Too
  • 2,502
  • 2
  • 16
  • 42
  • 1
    How come this answer is not higer voted? It does more or less the same as the accepted answer, but in a single migration. – KlausCPH Jul 17 '19 at 10:21
  • 1
    Anyway is not a good idea to keep data migration mixed with schema migration as [Django data migration doc](https://docs.djangoproject.com/en/3.0/topics/migrations/#data-migrations) says: "Migrations that alter data are usually called “data migrations”; they’re best written as separate migrations, sitting alongside your schema migrations". You can easily create an empty migration running `python manage.py makemigrations --empty` and execute the `RunPython` function there. – mathias.lantean Apr 14 '20 at 12:45
2

Here is a relatively complete example:

Step One

python manage.py makemigrations, set the temporary default value to None

Step Two

Change the genrated migration code to below style

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


def set_default_author_to_blog(apps, schema_editor):
    User = apps.get_model("auth", "User")
    Blog = apps.get_model("blog", "Blog")
    Blog.objects.update(author=User.objects.first())


def revert_set_default_autor_to_blog(apps, schema_editor):
    Blog = apps.get_model("blog", "Blog")
    Blog.objects.update(author=None)


class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('blog', '0001_auto_20220425_1017'),
    ]

    operations = [
        migrations.AddField(
            model_name='blog',
            name='author',
            field=models.ForeignKey(null=True, db_constraint=False, on_delete=django.db.models.deletion.PROTECT,
                                    to='auth.user', verbose_name='Author')
        ),
        migrations.RunPython(set_default_author_to_blog, reverse_code=revert_set_default_autor_to_blog),
        migrations.AlterField(
            model_name='blog',
            name='author',
            field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.PROTECT,
                                    to='auth.user', verbose_name='Author')
        ),
    ]

Step Three

python manage.py migrate

BaiJiFeiLong
  • 3,716
  • 1
  • 30
  • 28