Skip to content

Instantly share code, notes, and snippets.

@cb109
Last active February 9, 2024 18:42
Show Gist options
  • Save cb109/847df8376234fa02814debec9f3d26bc to your computer and use it in GitHub Desktop.
Save cb109/847df8376234fa02814debec9f3d26bc to your computer and use it in GitHub Desktop.
Fix: Cannot remove UniqueConstraint on ForeignKey with Django/MySQL

Django allows you to define custom UniqueConstraints to specify which combinations of values are allowed in a row, but removing these later can be problematic when some ForeignKey is involved, at least with MySQL it may throw a Cannot drop index '...': needed in a foreign key constraint at you.

The example below shows you how to resolve such a situation in 3 small individual migrations:

  • We start with an existing UniqueConstraint.
class MyModel(models.Model):
    other_model = models.ForeignKey("OtherModel", on_delete=models.CASCADE)
    name = models.CharField(max_length=128)

    class Meta:
        constraints = (
            models.UniqueConstraint(
                fields=(
                    "other_model",
                    "name",
                ),
                name="unique_other_model_name",
            ),
        )
  • Step 1: Tell Django that we don't want an index and constraint on the ForeignKey.
class MyModel(models.Model):
    other_model = models.ForeignKey(
        "OtherModel", 
        on_delete=models.CASCADE,
        db_index=False,
        db_constraint=False,
    )
    name = models.CharField(max_length=128)

    class Meta:
        constraints = (
            models.UniqueConstraint(
                fields=(
                    "other_model",
                    "name",
                ),
                name="unique_other_model_name",
            ),
        )
$ python manage.py makemigrations
  • Step 2: Remove the custom UniqueConstraint.
class MyModel(models.Model):
    other_model = models.ForeignKey(
        "OtherModel", 
        on_delete=models.CASCADE,
        db_index=False,
        db_constraint=False,
    )
    name = models.CharField(max_length=128)
$ python manage.py makemigrations
  • Step 3: Re-introduce the default behaviour of having an index and constraint on the ForeignKey.
class MyModel(models.Model):
    other_model = models.ForeignKey( "OtherModel", on_delete=models.CASCADE)
    name = models.CharField(Max_length=128)
$ python manage.py makemigrations

You'll end up with 3 migration files that can easily be combined into a single one before running this in production, to tidy things up.

Done!

@IhateTrains
Copy link

IhateTrains commented Dec 17, 2022

Thank you! I was afraid I fcked up real bad!

@cb109
Copy link
Author

cb109 commented Dec 17, 2022

Glad it was helpful, thanks for letting me know 👍

@gowthamvbhat
Copy link

Great gist!

@hamaike-biz
Copy link

It worked for me too, thanks.

@x-N0
Copy link

x-N0 commented Sep 20, 2023

Long time, no see!

We really have to find a better way to do this. Default behavior shouldn't be to revert a migration by adding another migration.

Glad you documented the trick.
Thx!

@mcursa-jwt
Copy link

this helped me loads! thank you :)

@manologg
Copy link

I have a problem with this: after the first migration is applied the index for the other_model is also removed and when the second migration tries to remove that constraint it can't... is there a way to skip errors of that kind? (something like "ignore if the RemoveConstraint tries to remove something that does not exist)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment