Django unique_together doesn't work with Forei

2019-02-13 13:49发布

问题:

I saw some ppl had this problem before me, but on older versions of Django, and I'm running on 1.2.1.

I have a model that looks like:

class Category(models.Model):
 objects = CategoryManager()

 name = models.CharField(max_length=30, blank=False, null=False)
 parent = models.ForeignKey('self', null=True, blank=True, help_text=_('The direct parent category.'))

 class Meta:
  unique_together = ('name', 'parent')

Whenever i try to save in the admin a category with a parent set to None, it still works when there's another category with the SAME name and parent set to None.

Ideas on how to solve this gracefully?

回答1:

The unique together constraint is enforced at the database level, and it appears that your database engine does not apply the constraint for null values.

In Django 1.2, you can define a clean method for your model to provide custom validation. In your case, you need something that checks for other categories with the same name whenever the parent is None.

class Category(models.Model):
    ...
    def clean(self):
        """
        Checks that we do not create multiple categories with 
        no parent and the same name.
        """
        from django.core.exceptions import ValidationError
        if self.parent and Category.objects.filter(name=self.name).exists():
            raise ValidationError("Another Category with name=%s and no parent already exists % self.name)

If you are editing categories through the Django admin, the clean method will be called automatically. In your own views, you must call category.fullclean().



回答2:

I had that problem too and solved it by creating a supermodel with clean method (like Alasdair suggested) and use it as base class for all my models:

class Base_model(models.Model):
  class Meta:
    abstract=True

  def clean(self):
    """
    Check for instances with null values in unique_together fields.
    """
    from django.core.exceptions import ValidationError

    super(Base_model, self).clean()

    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)