Уникальные поля, которые допускают нулевые значения в Django - PullRequest
112 голосов
/ 18 января 2009

У меня есть модель 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 с этим Bar уже существует».

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

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

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

EDIT

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

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

Ответы [ 8 ]

133 голосов
/ 09 сентября 2009

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
56 голосов
/ 20 декабря 2009

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

из документов :

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 в админке, - это создать подкласс 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 выдаст ошибку проверки (поле обязательно) или создаст столбец БД, который не принимает NULL.

12 голосов
/ 09 августа 2012

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

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

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

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

12 голосов
/ 26 июня 2010

Быстрое решение заключается в следующем:

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
6 голосов
/ 07 июля 2011

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

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

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)
2 голосов
/ 18 января 2009

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

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

1 голос
/ 22 июня 2010

У меня недавно было такое же требование. Вместо того, чтобы создавать подклассы для различных полей, я решил переопределить метод 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()    
0 голосов
/ 06 апреля 2019

Если у вас есть модель MyModel и вы хотите, чтобы my_field было пустым или уникальным, вы можете переопределить метод сохранения модели:

class MyModel(models.Model):
    my_field = models.TextField(unique=True, default=None, null=True, blank=True) 

    def save(self, **kwargs):
        self.my_field = self.my_field or None
        super().save(**kwargs)

Таким образом, поле не может быть пустым, будет только непустым или нулевым. нули не противоречат уникальности

...