25

I have a Django model which looks like this:

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

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

This works as expected; If there is the same name more than once in the same parent then I get an error: "MyModel with this Name and Parent already exists."

However, I also get an error when I save more than one MyModel with the same parent but with the name field blank, but this should be allowed. So basically I don't want to get the above error when the name field is blank. Is that possible somehow?

Mel
  • 5,837
  • 10
  • 37
  • 42
WesDec
  • 2,098
  • 4
  • 19
  • 23

5 Answers5

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 ('')

rombarcz
  • 1,604
  • 13
  • 19
  • Why you see the difference in behavior is because your name field is a CharField. CharFields don't store NULL, rather they are storing empty strings. A quick google search will lead you to the reason why this is. – dting Apr 25 '11 at 08:34
  • 1
    good call. whole thing is actually caused by lack of null on charfield – rombarcz Apr 25 '11 at 09:27
  • 2
    +1 I prefer a solution that has constraints enforced by the database. – MattH May 11 '11 at 20:08
  • 1
    Shouldn't you be inheriting from `models.CharField` rather than `forms.CharField`? Compare with [this answer](http://stackoverflow.com/questions/454436/unique-fields-that-allow-nulls-in-django/1934764#1934764). – Mechanical snail Sep 03 '11 at 00:33
  • 1
    No, it's not model field that I change behavior but FORM field. It is form that converts empty field to empty string '' which is perfectly good, but in case of uniqueness value of None (Null in database) is more suitable – rombarcz Sep 09 '11 at 18:51
  • Thanks a lot! This is the cleanest solution! – WesDec May 06 '12 at 14:38
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.

Matt Howell
  • 15,750
  • 7
  • 49
  • 56
  • 3
    i think using .exists is the preferred way, over len(queryset). – dting Apr 24 '11 at 20:20
  • @bigmatty: This solution is not good since it means that if you want to change some other fields of your instance later and save them using .save(), then you will get an exception. – WesDec May 11 '11 at 10:46
  • @WesDec: You should only get an exception when you're trying to save an instance with the same `parent` and `name` as another object -- of course, unless the `name` attribute is an empty string. That's the behavior you said you wanted, no? – Matt Howell May 11 '11 at 17:18
  • @bigmattyh: Yes but imagine that i create an instance where 'parent' and 'name' are unique and the name is not an empty string. If i later get the same instance from the database and change a field and then save the instance to the database, the same check for 'parent' and 'name' will be done again, and this time i will get an exception. – WesDec May 11 '11 at 19:02
  • @WesDec: That's not that hard to fix. See above for the edited code to handle this case. – Matt Howell May 11 '11 at 19:37
  • Note that this still has a race condition. We've seen duplicates getting created in production by this code. – mlissner Jul 14 '17 at 16:57
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,098
  • 4
  • 19
  • 23
  • The only problem with this solution is that `clean()` is not called when a model's `save()` method is called (see the [docs](https://docs.djangoproject.com/en/1.11/ref/models/instances/#django.db.models.Model.clean)). I never really understood why though, because it does seem like a cleaner (no pun intended) solution. – Mathieu Dhondt Dec 05 '17 at 19:47
  • You can call `self.clean()` in the `model's` `save` method – Carl Brubaker Feb 25 '19 at 03:16
1

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
  • 38,604
  • 10
  • 95
  • 114