Django: 'unique_together' и 'blank = True'

У меня есть модель Django, которая выглядит так:

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

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

Это работает так, как ожидалось; Если один и тот же name более одного раза в одном и том же parent, то я получаю сообщение об ошибке: "MyModel с этим именем и родителем уже существует".

Однако я также получаю сообщение об ошибке, когда я сохраняю более одного MyModel с тем же parent, но с полем name, но это должно быть разрешено. Поэтому в основном я не хочу получать вышеуказанную ошибку, когда поле name пустое. Возможно ли это как-то?

Ответы

Ответ 1

Во-первых, blank (пустая строка) НЕ ТОЛЬКО как null ('' != None).

Во-вторых, Django CharField при использовании через формы будет хранить пустую строку, когда вы оставите поле пустым.

Итак, если ваше поле было чем-то другим, чем CharField, вы должны просто добавить null=True к нему. Но в этом случае вам нужно сделать больше. Вам нужно создать подкласс forms.CharField и переопределить его метод clean, чтобы вернуть None в пустой строке, примерно так:

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

а затем используйте его в форме для модели ModelForm:

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

таким образом, если вы оставите его пустым, он будет хранить null в базе данных вместо пустой строки ('')

Ответ 2

Используя unique_together, вы сообщаете Django, что вам не нужны два экземпляра MyModel с теми же атрибутами parent и name, которые применяются, даже если name - пустая строка.

Это выполняется на уровне базы данных с использованием атрибута unique в соответствующих столбцах базы данных. Поэтому, чтобы делать какие-либо исключения из этого поведения, вам придется избегать использования unique_together в вашей модели.

Вместо этого вы можете получить то, что хотите, переопределив метод save на модели и обеспечив там уникальную сдержанность. Когда вы пытаетесь сохранить экземпляр своей модели, ваш код может проверить, имеются ли какие-либо существующие экземпляры, которые имеют те же комбинации parent и name, и отказываются сохранять экземпляр, если таковые имеются. Но вы также можете разрешить сохранение экземпляра, если name - пустая строка. Базовая версия этого может выглядеть так:

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)

Надеюсь, что это поможет.

Ответ 3

Это решение очень похоже на то, которое задано @bigmattyh, однако я нашел нижеприведенную страницу, где описывается, где должна выполняться проверка:

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

Решение я в конечном итоге использует следующее:

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

Обратите внимание, что ValidationError возникает вместо общего исключения. Это решение имеет то преимущество, что при проверке ModelForm с использованием .is_valid() метод models.clean() выше автоматически вызывается и сохранит строку ValidationError в .errors, чтобы ее можно было отобразить в html-шаблоне.

Сообщите мне, если вы не согласны с этим решением.

Ответ 4

bigmattyh дает хорошее объяснение тому, что происходит. Я просто добавлю способ save.

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)

Я думаю, что я решил сделать что-то подобное, переопределив мой метод чистой модели, и он выглядел примерно так:

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