[Django]-Django: 'unique_together' and 'blank=True'

18👍

Firstly, blank (empty string) IS NOT same as null ('' != None).

Secondly, Django CharField when used through forms will be storing empty string when you leave field empty.

So if your field was something else than CharField you should just add null=True to it. But in this case you need to do more than that. You need to create subclass of forms.CharField and override it’s clean method to return None on empty string, something like this:

class NullCharField(forms.CharField):
    def clean(self, value):
        value = super(NullCharField, self).clean(value)
        if value in forms.fields.EMPTY_VALUES:
            return None
        return value

and then use it in form for your ModelForm:

class MyModelForm(forms.ModelForm):
    name = NullCharField(required=False, ...)

this way if you leave it blank it will store null in database instead of empty string ('')

13👍

Using unique_together, you’re telling Django that you don’t want any two MyModel instances with the same parent and name attributes — which applies even when name is an empty string.

This is enforced at the database level using the unique attribute on the appropriate database columns. So to make any exceptions to this behavior, you’ll have to avoid using unique_together in your model.

Instead, you can get what you want by overriding the save method on the model and enforcing the unique restraint there. When you try to save an instance of your model, your code can check to see if there are any existing instances that have the same parent and name combination, and refuse to save the instance if there are. But you can also allow the instance to be saved if the name is an empty string. A basic version of this might look like this:

class MyModel(models.Model):
    ...

    def save(self, *args, **kwargs):

        if self.name != '':
            conflicting_instance = MyModel.objects.filter(parent=self.parent, \
                                                          name=self.name)
            if self.id:
                # This instance has already been saved. So we need to filter out
                # this instance from our results.
                conflicting_instance = conflicting_instance.exclude(pk=self.id)

            if conflicting_instance.exists():
                raise Exception('MyModel with this name and parent already exists.')

        super(MyModel, self).save(*args, **kwargs)

Hope that helps.

2👍

This solution is very similar to the one given by @bigmattyh, however, i found the below page which describes where the validation should be done:

http://docs.djangoproject.com/en/1.3/ref/models/instances/#validating-objects

The solution i ended up using is the following:

from django    import forms

class MyModel(models.Model):
...

def clean(self):
    if self.name != '':
        instance_exists = MyModel.objects.filter(parent=self.parent,
                                                 name=self.name).exists()
        if instance_exists:
            raise forms.ValidationError('MyModel with this name and parent already exists.')

Notice that a ValidationError is raised instead of a generic exception. This solution has the benefit that when validating a ModelForm, using .is_valid(), the models .clean() method above is automatically called, and will save the ValidationError string in .errors, so that it can be displayed in the html template.

Let me know if you do not agree with this solution.

👤WesDec

2👍

You can use constraints to set up a partial index like so:

class MyModel(models.Model):
    parent = models.ForeignKey(ParentModel)
    name   = models.CharField(blank=True, max_length=200)
    ... other fields ...

    class Meta:    
      constraints = [
        models.UniqueConstraint(
          fields=['name', 'parent'],
          condition=~Q(name='')
          name='unique_name_for_parent'
        )
      ]

This allow constraints like UniqueTogether to only apply to certain rows (based on conditions you can define using Q).

Incidentally, this happens to be the Django recommended path forward as well: https://docs.djangoproject.com/en/3.2/ref/models/options/#unique-together

Some more documentation: https://docs.djangoproject.com/en/3.2/ref/models/constraints/#django.db.models.UniqueConstraint

-1👍

bigmattyh gives a good explanation as to what is happening. I’ll just add a possible save method.

def save(self, *args, **kwargs):
    if self.parent != None and MyModels.objects.filter(parent=self.parent, name=self.name).exists():
        raise Exception('MyModel with this name and parent exists.')
    super(MyModel, self).save(*args, **kwargs)

I think I chose to do something similar by overriding my model’s clean method and it looked something like this:

from django.core.exceptions import ValidationError
def clean(self):
    if self.parent != None and MyModels.objects.filter(parent=self.parent, name=self.name).exists():
        raise ValidationError('MyModel with this name and parent exists.')
👤dting

Leave a comment