Reputation: 12378
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
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:
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
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
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:
Null
value for foreign key and use some default valuesave
method of your model to check that no such row existsUpvotes: 26
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