Уникальный сдерживающий фактор Django?

Использование Django 1.5.1. Python 2.7.3.

Я хотел сделать уникальное ограничение вместе с полем внешнего ключа и полем слияния. Так что в моей метате модели я сделал

foreign_key = models.ForeignKey("self", null=True, default=None)
slug = models.SlugField(max_length=40, unique=False)

class Meta:
    unique_together = ("foreign_key", "slug")

Я даже проверил описание таблицы в Postgres (9.1), и ограничение было помещено в таблицу базы данных.

-- something like
"table_name_foreign_key_id_slug_key" UNIQUE CONSTRAINT, btree (foreign_key_id, slug)

Однако я все же мог сохранить в таблице базы данных foreign_key из None/null и дублировать строки.

Например,

Я мог бы ввести и сохранить

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

Итак, после использования unique_together, почему я могу по-прежнему вводить три одинаково ценные строки?

Я просто догадываюсь прямо сейчас, что это может иметь отношение к значению по умолчанию None для поля foreign_key, потому что перед unique_together, когда у меня только что было уникальное value = True на slug, все работало нормально. Итак, если это так, то какое значение по умолчанию должно иметь значение, указывающее нулевое значение, но также поддерживающее уникальное ограничение?

Ответ 1

В Postgresql NULL не равно никакому другому NULL. Поэтому созданные вами строки не совпадают (с точки зрения Postgres).

Обновление

У вас есть несколько способов справиться с этим:

  • Запретить значение NULL для внешнего ключа и использовать значение по умолчанию
  • Переопределите метод save вашей модели, чтобы проверить, нет ли такой строки.
  • Изменить стандарт SQL:)

Ответ 2

Добавьте к вашей модели метод clean, чтобы вы могли редактировать существующую строку.

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")

Остерегайтесь, clean (или full_clean) не вызывается методом save по умолчанию.

Примечание: если вы поместите этот код в метод save, формы обновления (например, в админе) не будут работать: у вас будет ошибка трассировки из-за исключения ValidationError.

Ответ 3

Просто вручную создайте вторичный индекс в поле slug, но только для значений NULL в foreign_key_id:

CREATE INDEX table_name_unique_null_foreign_key
  ON table_name (slug) WHERE foreign_key_id is NULL

Обратите внимание, что Django не поддерживает это, поэтому без пользовательской проверки формы/модели вы получите чистый IntegrityError/500.

Возможный дубликат Создать уникальное ограничение с нулевыми столбцами

Ответ 4

Как упоминалось выше, "В Postgresql NULL не равно никому другому NULL. Поэтому создаваемые вами строки не совпадают (с точки зрения Postgres).

Другим возможным способом решения этой проблемы является добавление пользовательской проверки на уровне представления в методе form_valid.

В 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)