Уникальные поля, допускающие нули в Django

У меня есть модель Foo, которая имеет панель полей. Поле bar должно быть уникальным, но допускать нули в нем, то есть я хочу разрешить более одной записи, если поле bar null, но если это не null, значения должны быть уникальными.

Вот моя модель:

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)

И вот соответствующий SQL для таблицы:

CREATE TABLE appl_foo
(
    id serial NOT NULL,
     "name" character varying(40) NOT NULL,
    bar character varying(40),
    CONSTRAINT appl_foo_pkey PRIMARY KEY (id),
    CONSTRAINT appl_foo_bar_key UNIQUE (bar)
)   

При использовании интерфейса администратора для создания более 1 объекта foo, где bar имеет значение null, он дает мне ошибку: "Foo с этой панелью уже существует".

Однако когда я вставляю в базу данных (PostgreSQL):

insert into appl_foo ("name", bar) values ('test1', null)
insert into appl_foo ("name", bar) values ('test2', null)

Это работает, просто отлично, это позволяет мне вставлять более 1 записи с нулевым баром, поэтому база данных позволяет мне делать то, что я хочу, это просто что-то не так с моделью Django. Любые идеи?

ИЗМЕНИТЬ

Переносимость решения по БД не является проблемой, мы довольны Postgres. Я пробовал настройку, уникальную для вызываемого, которая была моей функцией, возвращающей True/False для определенных значений бара, она не выдавала никаких ошибок, однако такой шов не имел никакого эффекта.

До сих пор я удалял уникальный спецификатор из свойства bar и обрабатывал уникальность штриха в приложении, однако все еще искал более элегантное решение. Любые рекомендации?

Ответ 1

Django не считал NULL равным NULL для целей проверок уникальности, так как билет # 9039 был исправлен, см.

http://code.djangoproject.com/ticket/9039

Проблема здесь в том, что нормализованное "пустое" значение для формы CharField - это пустая строка, а не None. Поэтому, если вы оставите поле пустым, вы получите пустую строку, а не NULL, хранящуюся в БД. Пустые строки равны пустым строкам для проверок уникальности, как в правилах Django, так и в базе данных.

Вы можете заставить интерфейс администратора хранить NULL для пустой строки, предоставив свою собственную настраиваемую форму модели для Foo с помощью метода clean_bar, который превращает пустую строку в None:

class FooForm(forms.ModelForm):
    class Meta:
        model = Foo
    def clean_bar(self):
        return self.cleaned_data['bar'] or None

class FooAdmin(admin.ModelAdmin):
    form = FooForm

Ответ 2

** edit 11/30/2015. В python 3 глобальная переменная __metaclass__ модуля больше не поддерживается. Кроме того, с Django 1.10 класс SubfieldBase был устарел:

из docs:

django.db.models.fields.subclassing.SubfieldBase устарел и будет удален в Django 1.10. Исторически он использовался для обработки полей, где требуется преобразование типов при загрузке из базы данных, но он не использовался в вызовах .values() или в совокупности. Он был заменен на from_db_value(). Обратите внимание, что новый подход не вызывает метод to_python() при назначении, как в случае с SubfieldBase.

Поэтому, как было предложено from_db_value() документация, и этот пример, это решение должно быть изменено на:

class CharNullField(models.CharField):

    """
    Subclass of the CharField that allows empty strings to be stored as NULL.
    """

    description = "CharField that stores NULL but returns ''."

    def from_db_value(self, value, expression, connection, contex):
        """
        Gets value right out of the db and changes it if its ``None``.
        """
        if value is None:
            return ''
        else:
            return value


    def to_python(self, value):
        """
        Gets value right out of the db or an instance, and changes it if its ``None``.
        """
        if isinstance(value, models.CharField):
            # If an instance, just return the instance.
            return value
        if value is None:
            # If db has NULL, convert it to ''.
            return ''

        # Otherwise, just return the value.
        return value

    def get_prep_value(self, value):
        """
        Catches value right before sending to db.
        """
        if value == '':
            # If Django tries to save an empty string, send the db None (NULL).
            return None
        else:
            # Otherwise, just pass the value.
            return value

Я думаю, что лучший способ, чем переопределить cleaned_data в admin, будет заключаться в подклассе charfield - таким образом, независимо от того, какая форма обращается к полю, он будет "просто работать". Вы можете поймать '' непосредственно перед отправкой в ​​базу данных и поймать NULL сразу после выхода из базы данных, а остальная часть Django не будет знать/ухаживать. Быстрый и грязный пример:

from django.db import models


class CharNullField(models.CharField):  # subclass the CharField
    description = "CharField that stores NULL but returns ''"
    __metaclass__ = models.SubfieldBase  # this ensures to_python will be called

    def to_python(self, value):
        # this is the value right out of the db, or an instance
        # if an instance, just return the instance
        if isinstance(value, models.CharField):
            return value 
        if value is None:  # if the db has a NULL (None in Python)
            return ''      # convert it into an empty string
        else:
            return value   # otherwise, just return the value

    def get_prep_value(self, value):  # catches value right before sending to db
        if value == '':   
            # if Django tries to save an empty string, send the db None (NULL)
            return None
        else:
            # otherwise, just pass the value
            return value  

Для моего проекта я бросил это в файл extras.py, который живет в корне моего сайта, тогда я могу просто from mysite.extras import CharNullField в моем приложении models.py. Поле действует так же, как CharField - просто не забудьте установить blank=True, null=True при объявлении поля, иначе Django будет вызывать ошибку проверки (требуется поле) или создать столбец db, который не принимает NULL.

Ответ 3

Быстрое исправление:

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

    if not self.bar:
        self.bar = None

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

Ответ 4

Поскольку я новичок в stackoverflow, мне еще не разрешено отвечать на ответы, но я хотел бы указать, что с философской точки зрения я не могу согласиться с самым популярным ответом на этот вопрос. (Карен Трейси)

OP требует, чтобы его поле bar было уникальным, если оно имеет значение, а null - иначе. Тогда должно быть, что сама модель гарантирует, что это так. Это не может быть оставлено для внешнего кода, чтобы проверить это, потому что это означает, что его можно обойти. (Или вы можете забыть проверить его, если вы в будущем будете писать новое представление)

Поэтому, чтобы ваш код действительно OOP, вы должны использовать внутренний метод вашей модели Foo. Изменение метода save() или поля является хорошим вариантом, но использование формы для этого, безусловно, не является.

Лично я предпочитаю использовать предложенный CharNullField для переносимости моделей, которые я мог бы определить в будущем.

Ответ 5

Другое возможное решение

class Foo(models.Model):
    value = models.CharField(max_length=255, unique=True)

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)

Ответ 6

К лучшему или худшему, Django считает, что NULL эквивалентен NULL для целей проверок уникальности. На самом деле не обойтись без написания собственной реализации проверки уникальности, которая считает, что NULL является уникальной, независимо от того, сколько раз она происходит в таблице.

(и имейте в виду, что некоторые решения БД принимают одинаковый вид NULL, поэтому код, основанный на одном представлении БД о NULL, может быть не переносимым для других)

Ответ 7

Недавно у меня было такое же требование. Вместо того, чтобы подклассифицировать разные поля, я решил переопределить метод save() на моей модели (с именем "MyModel" ниже) следующим образом:

def save(self):
        """overriding save method so that we can save Null to database, instead of empty string (project requirement)"""
        # get a list of all model fields (i.e. self._meta.fields)...
        emptystringfields = [ field for field in self._meta.fields \
                # ...that are of type CharField or Textfield...
                if ((type(field) == django.db.models.fields.CharField) or (type(field) == django.db.models.fields.TextField)) \
                # ...and that contain the empty string
                and (getattr(self, field.name) == "") ]
        # set each of these fields to None (which tells Django to save Null)
        for field in emptystringfields:
            setattr(self, field.name, None)
        # call the super.save() method
        super(MyModel, self).save()