52

I'm using Django 1.8.4 in my dev machine using Sqlite and I have these models:

class ModelA(Model):
    field_a = CharField(verbose_name='a', max_length=20)
    field_b = CharField(verbose_name='b', max_length=20)

    class Meta:
        unique_together = ('field_a', 'field_b',)


class ModelB(Model):
    field_c = CharField(verbose_name='c', max_length=20)
    field_d = ForeignKey(ModelA, verbose_name='d', null=True, blank=True)

    class Meta:
        unique_together = ('field_c', 'field_d',)

I've run proper migration and registered them in the Django Admin. So, using the Admin I've done this tests:

  • I'm able to create ModelA records and Django prohibits me from creating duplicate records - as expected!
  • I'm not able to create identical ModelB records when field_b is not empty
  • But, I'm able to create identical ModelB records, when using field_d as empty

My question is: How do I apply unique_together for nullable ForeignKey?

The most recent answer I found for this problem has 5 year... I do think Django have evolved and the issue may not be the same.

Ivan
  • 5,803
  • 2
  • 29
  • 46
MatheusJardimB
  • 3,599
  • 7
  • 46
  • 70
  • Possible duplicate of [Django unique together constraint failure?](http://stackoverflow.com/questions/17510261/django-unique-together-constraint-failure) – Ivan Oct 23 '15 at 17:25
  • @Ivan I've tried this post too, but it did not helped me with the modelformset. When trying to create identical records in the same formset, Django doesn't prohibit. The user is still able to create duplicate items – MatheusJardimB Oct 23 '15 at 17:42
  • "I'm not able to create identical ModelB records when field_b is not empty" Did you mean "field_d"? – vmonteco Oct 08 '21 at 15:44

6 Answers6

77

Django 2.2 added a new constraints API which makes addressing this case much easier within the database.

You will need two constraints:

  1. The existing tuple constraint; and
  2. The remaining keys minus the nullable key, with a condition

If you have multiple nullable fields, I guess you will need to handle the permutations.

Here's an example with a thruple of fields that must be all unique, where only one NULL is permitted:

from django.db import models
from django.db.models import Q
from django.db.models.constraints import UniqueConstraint

class Badger(models.Model):
    required = models.ForeignKey(Required, ...)
    optional = models.ForeignKey(Optional, null=True, ...)
    key = models.CharField(db_index=True, ...)

    class Meta:
        constraints = [
            UniqueConstraint(fields=['required', 'optional', 'key'],
                             name='unique_with_optional'),
            UniqueConstraint(fields=['required', 'key'],
                             condition=Q(optional=None),
                             name='unique_without_optional'),
        ]
djvg
  • 11,722
  • 5
  • 72
  • 103
Danielle Madeley
  • 2,616
  • 1
  • 19
  • 26
  • This is by far the cleanest way to handle such scenarios. – cantordust Mar 19 '20 at 06:04
  • This should be the preferred answer. – loicgasser Apr 21 '20 at 14:24
  • 2
    Great answer. Too bad MySQL does not support it :( – gabn88 May 16 '20 at 12:49
  • Great answer, but in my case in the same migration should have been created a referenced field, and this step was placed after creation of constraints, it caused exception `django.core.exceptions.FieldDoesNotExist`. I had to manually reorder steps in migration and it worked for me. – Glebsa Jun 04 '20 at 19:36
  • For those interested, this is the best answer I found re: raw SQL https://dba.stackexchange.com/a/9760/212269 – alias51 Jul 12 '20 at 20:06
  • Also, as stated in the [djanog-docs](https://docs.djangoproject.com/en/dev/ref/models/options/#unique-together) `unique_together` may eventually be deprecated in favor of `UniqueConstraints`. So `UniqueConstraints` seems like the preferred solution, in general. – Kim Dec 15 '20 at 16:21
  • Not sure about MySQL , but in MariaDB 10.x , a composite unique constraint ([unique index](https://mariadb.com/kb/en/getting-started-with-indexes/#unique-index)) can be NULL if one of the columns is NULL. The code in this answer can work in MariaDB for composite unique constraint with many foreign keys. – Ham Dec 22 '20 at 05:42
24

UPDATE: previous version of my answer was functional but had bad design, this one takes in account some of the comments and other answers.

In SQL NULL does not equal NULL. This means if you have two objects where field_d == None and field_c == "somestring" they are not equal, so you can create both.

You can override Model.clean to add your check:

class ModelB(Model):
    #...
    def validate_unique(self, exclude=None):
        if ModelB.objects.exclude(id=self.id).filter(field_c=self.field_c, \
                                 field_d__isnull=True).exists():
            raise ValidationError("Duplicate ModelB")
        super(ModelB, self).validate_unique(exclude)

If used outside of forms you have to call full_clean or validate_unique.

Take care to handle the race condition though.

Ivan
  • 5,803
  • 2
  • 29
  • 46
  • Tx @Ivan, but should I be able to creat two ModelB records with `field_c=="a", field_d==None` ?? – MatheusJardimB Oct 23 '15 at 17:17
  • And how can I change this behaviour :) – MatheusJardimB Oct 23 '15 at 17:18
  • tx! It is still not solved for me. I'm using a formset and just by overriding clean method, when the user fills two forms both are valid (formset.is_valid()) but when the first is saved the second one becomes invalid. You get my point? Any tip regarding formsets and this post? – MatheusJardimB Oct 23 '15 at 20:40
  • I think `validate_unique` would be a better method to override than `clean`...no? – Dustin Wyatt Feb 09 '17 at 16:23
  • It works for me but I had to check that the instance I was running validation on had its nullable field set to null. Which looks missing here. – vmonteco Oct 11 '18 at 08:59
10

@ivan, I don't think that there's a simple way for django to manage this situation. You need to think of all creation and update operations that don't always come from a form. Also, you should think of race conditions...

And because you don't force this logic on DB level, it's possible that there actually will be doubled records and you should check it while querying results.

And about your solution, it can be good for form, but I don't expect that save method can raise ValidationError.

If it's possible then it's better to delegate this logic to DB. In this particular case, you can use two partial indexes. There's a similar question on StackOverflow - Create unique constraint with null columns

So you can create Django migration, that adds two partial indexes to your DB

Example:

# Assume that app name is just `example`

CREATE_TWO_PARTIAL_INDEX = """
    CREATE UNIQUE INDEX model_b_2col_uni_idx ON example_model_b (field_c, field_d)
    WHERE field_d IS NOT NULL;

    CREATE UNIQUE INDEX model_b_1col_uni_idx ON example_model_b (field_c)
    WHERE field_d IS NULL;
"""

DROP_TWO_PARTIAL_INDEX = """
    DROP INDEX model_b_2col_uni_idx;
    DROP INDEX model_b_1col_uni_idx;
"""


class Migration(migrations.Migration):

    dependencies = [
        ('example', 'PREVIOUS MIGRATION NAME'),
    ]

    operations = [
        migrations.RunSQL(CREATE_TWO_PARTIAL_INDEX, DROP_TWO_PARTIAL_INDEX)
    ]
Artem Bernatskyi
  • 4,185
  • 2
  • 26
  • 35
vvkuznetsov
  • 1,056
  • 1
  • 9
  • 16
  • Yes, you are right, and in my other answers I usually add that it will only work if `clean` is called. On the other hand I generally try to avoid having sneaky db modifications outside the model code, except for data migrations. – Ivan Feb 09 '17 at 17:59
  • +1 Data integrity constraints should live as close to the data as possible, and the best way to do that is with a DB constraint. Any python approach (like the accepted answer) will have race condition issues. – chukkwagon Jul 27 '17 at 20:02
  • Can this be done with Mysql? My understanding is that there is no feature like "Where is NULL" when creating unique constraints with Mysql – Patrick Kenekayoro Oct 08 '17 at 13:23
  • @PatrickKenekayoro, no, MySQL has no such thing as a partial index. Look into that for more info https://dba.stackexchange.com/a/106593 Only reasonable solution is to make this functionality as a part of your code base, like suggested Ivan – vvkuznetsov Oct 10 '17 at 09:53
1

Add a clean method to your model - see below:

def clean(self):
        if Variants.objects.filter("""Your filter """).exclude(pk=self.pk).exists():
            raise ValidationError("This variation is duplicated.")
J. M. Arnold
  • 6,261
  • 3
  • 20
  • 38
Yousef Alm
  • 238
  • 1
  • 8
0

I think this is more clear way to do that for Django 1.2+

In forms it will be raised as non_field_error with no 500 error, in other cases, like DRF you have to check this case manual, because it will be 500 error. But it will always check for unique_together!

class BaseModelExt(models.Model):
is_cleaned = False

def clean(self):
    for field_tuple in self._meta.unique_together[:]:
        unique_filter = {}
        unique_fields = []
        null_found = False
        for field_name in field_tuple:
            field_value = getattr(self, field_name)
            if getattr(self, field_name) is None:
                unique_filter['%s__isnull' % field_name] = True
                null_found = True
            else:
                unique_filter['%s' % field_name] = field_value
                unique_fields.append(field_name)
        if null_found:
            unique_queryset = self.__class__.objects.filter(**unique_filter)
            if self.pk:
                unique_queryset = unique_queryset.exclude(pk=self.pk)
            if unique_queryset.exists():
                msg = self.unique_error_message(self.__class__, tuple(unique_fields))

                raise ValidationError(msg)

    self.is_cleaned = True

def save(self, *args, **kwargs):
    if not self.is_cleaned:
        self.clean()

    super().save(*args, **kwargs)
MegaJoe
  • 575
  • 6
  • 11
0

One possible workaround not mentioned yet is to create a dummy ModelA object to serve as your NULL value. Then you can rely on the database to enforce the uniqueness constraint.

cjerdonek
  • 5,814
  • 2
  • 32
  • 26