Derek
Derek

Reputation: 12378

Django unique together constraint failure?

Using Django 1.5.1. Python 2.7.3.

I wanted to do a unique together constraint with a foreign key field and a slug field. So in my model meta, I did

foreign_key = models.ForeignKey("self", null=True, default=None)
slug = models.SlugField(max_length=40, unique=False)

class Meta:
    unique_together = ("foreign_key", "slug")

I even checked the table description in Postgres (9.1) and the constraint was put into the database table.

-- something like
"table_name_foreign_key_id_slug_key" UNIQUE CONSTRAINT, btree (foreign_key_id, slug)

However, I could still save into the database table a foreign_key of None/null and duplicate strings.

For example,

I could input and save

# model objects with slug="python" three times; all three foreign_key(s) 
# are None/null because that is their default value
MO(slug="python").save()
MO(slug="python").save()
MO(slug="python").save()

So after using unique_together, why can I still input three of the same valued rows?

I'm just guessing right now that it might have to do with the default value of None for the foreign_key field, because before the unique_together, when I just had unique=True on slug, everything worked fine. So if that is the case, what default value should I have that indicates a null value, but also maintains the unique constraint?

Upvotes: 18

Views: 13099

Answers (4)

sv_rancher
sv_rancher

Reputation: 161

As hobbyte mentioned, "In Postgresql NULL isn't equal to any other NULL. Therefore the rows you create are not the same (from Postgres' perspective)."

Another possible way to address this challenge is to add custom validation at the view level in the form_valid method.

In views.py:

def form_valid(self, form): 

  --OTHER VALIDATION AND FIELD VALUE ASSIGNMENT LOGIC--

  if ModelForm.objects.filter(slug=slug,foreign_key=foreign_key:   
    form.add_error('field',
      forms.ValidationError( _("Validation error message that shows up in your form. "), 
      code='duplicate_row', )) 
    return self.form_invalid(form)

This approach is helpful if you are using class based views, especially if you are automatically assigning values to fields that you want to hide from the user.

Pros:

  • You don't have to create dummy default values in the database
  • You can still use update forms (see Toff's answer)

Cons: - This doesn't protect against duplicate rows created directly at the database level. - If you use Django's admin backend to create new MyModel objects, you'll need to add this same validation logic to your admin form.

Upvotes: 0

marcinn
marcinn

Reputation: 546

Just manually create secondary index on slug field, but only for NULL values in foreign_key_id:

CREATE INDEX table_name_unique_null_foreign_key
  ON table_name (slug) WHERE foreign_key_id is NULL

Please note, that Django does not support this, so without custom form/model validation you will get pure IntegrityError / 500.

Possible duplicate of Create unique constraint with null columns

Upvotes: 0

Yossi
Yossi

Reputation: 12090

In Postgresql NULL isn't equal to any other NULL. Therefore the rows you create are not the same (from Postgres' perspective).

Update

You have a few ways to deal with it:

  • Forbid the Null value for foreign key and use some default value
  • Override the save method of your model to check that no such row exists
  • Change SQL standard :)

Upvotes: 26

Toff'
Toff'

Reputation: 489

Add a clean method to your model, so you can edit an existing row.

def clean(self):
    queryset = MO.objects.exclude(id=self.id).filter(slug=self.slug)
    if self.foreign_key is None:
        if queryset.exists():
            raise ValidationError("A row already exists with this slug and no key")
    else:
        if queryset.filter(foreign_key=self.foreign_key).exists():
            raise ValidationError("This row already exists")

Beware, clean (or full_clean) isn't called by the default save method.

NB: if you put this code in the save method, update forms (like in the admin) won't work: you will have a traceback error due to the ValidationError exception.

Upvotes: 1

Related Questions