Django change an existing field to foreign key

I have a Django model that used to look like this:

class Car(models.Model):
    manufacturer_id = models.IntegerField()

There is another model called Manufacturer that the id field refers to. However, I realized that it would be useful to use Django's built-in foreign key functionality, so I changed the model to this:

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer)

This change appears to work fine immediately, queries work without errors, but when I try to run migrations, Django outputs the following:

- Remove field manufacturer_id from car
- Add field manufacturer to car

Doing this migration would clear all the existing relationships in the database, so I don't want to do that. I don't really want any migrations at all, since queries like Car.objects.get(manufacturer__name="Toyota") work fine. I would like a proper database foreign key constraint, but it's not a high priority.

So my question is this: Is there a way to make a migration or something else that allows me to convert an existing field to a foreign key? I cannot use --fake since I need to reliably work across dev, prod, and my coworkers' computers.

Upvotes: 5

Views: 7766

Answers (3)

glee8e
glee8e

Reputation: 6419

Since the field won't receive any real changes (that is, all SQL would be emitted is effectively a noop when combined), you could simply tell Django not to run any SQL at all, and only change the in-python model state. This is what your model migration would look like


class Migration(migrations.Migration):

    dependencies = [
        ('device', '0017_auto_20240723_0936'),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.RemoveField(
                    model_name='car',
                    name='manufacturer_id',
                ),
                migrations.AddField(
                    model_name='car',
                    name='manufacturer',
                    field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='device.SinglePhaseDevice'),
                ),
            ],
            database_operations=[]
        )
    ]

Be aware of the meaning of this action though. You are effectively changing the model without running any actual DDL. In certain (broken) database configurations that list must be populated with something. Extra attention, testing and reviewing is REQUIRED to ensure database_operations is indeed empty.

A simple way to verify

  1. prepare an empty database.
  2. Perform migration up to the latest migration.
  3. run the django generated migration. Save the new table DDL somewhere. Then rollback this django generated migration.
  4. run the handwritten migration. Compare the current DDL with the one you just saved.

The two DDLs should be functionally equivalent barring field orders (due to django generated one being likely to include REMOVE field statements, messing up field orders). If they aren't, try add some migrations.AlterField() ops to database_operations until they match up with each other.

Upvotes: 1

zealotous
zealotous

Reputation: 36

I've faced the same issue today. Can be done without renaming an existing field or dropping existing column.

  1. Update the initial migration with CreateModel operation
...
        migrations.CreateModel(
            name="Car",
            fields=[
                (
                    "manufacturer",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.DO_NOTHING,
                        db_constraint=False,
                        db_index=False,
                        to="Manufacturer",
                    ),
                ),
  1. Check that db_constraint and db_index are True on Car.manufacturer field of Car model. True is a default value if the fields not set.
  2. Run ./manage.py makemigrations that generates AlterField migration with required constraint and index on Car.manufacturer_id.

The first step won't effect db consistency because the generated DDL will be the same for IntegerField and ForeignKey with db_constaint=False and db_index=False and the second migration adds missing constraint and index.

You can check that with ./manage.py sqlmigrate app migration command

Upvotes: -1

Linum
Linum

Reputation: 351

You can do data migration

  1. add new field
  2. do a data migration https://docs.djangoproject.com/en/3.1/topics/migrations/#data-migrations
  3. remove old field

I am not sure, there might be another solution where you can rename the field to name you want to, then alter the filed to new type (do a migration)

operations = [
        migrations.RenameField(
            model_name='car',
            old_name='manufacturer_id',
            new_name='manufacturer',
        ),
        migrations.AlterField(
            model_name='car',
            name='manufacturer',
            field=ForeignKey(blank=True, null=True,  
                  on_delete=django.db.models.deletion.CASCADE
            ),
    ]

Upvotes: 9

Related Questions