Уникальные поля, которые допускают нулевые значения в Django


135

У меня есть модель 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. Любые идеи?

РЕДАКТИРОВАТЬ

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

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


Я пока не могу комментировать, поэтому здесь небольшое дополнение к mightyhal: Начиная с Django 1.4 вам понадобится def get_db_prep_value(self, value, connection, prepared=False)как вызов метода. Проверьте groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ для получения дополнительной информации. У меня тоже работает следующий метод: def get_prep_value (self, value): if value == "": # Если Django пытается сохранить '' строку, отправьте db None (NULL), верните None else: верните значение #otherwise, просто передать значение
Jens

Я открыл билет на Джанго для этого. Добавьте вашу поддержку. code.djangoproject.com/ticket/30210#ticket
Карл Брубейкер,

Ответы:


154

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
Если строка пуста, замените ее на None в методе pre_save. Код будет более сухим, я полагаю.
Ашиш Гупта

6
Этот ответ помогает только для ввода данных на основе форм, но ничего не делает для защиты целостности данных. Данные могут быть введены через скрипты импорта, из оболочки, через API или любым другим способом. Гораздо лучше переопределить метод save (), чем создавать собственные случаи для каждой формы, которая может касаться данных.
шейкер

Django 1.9+ требует атрибута fieldsили excludeв ModelFormинстансах. Вы можете обойти это, опуская Metaвнутренний класс из ModelForm для использования в админке. Справка: docs.djangoproject.com/en/1.10/ref/contrib/admin/...
user85461

62

** edit 30.11.2015 : В python 3 глобальная __metaclass__переменная модуля больше не поддерживается . Additionaly, по состоянию 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 выдаст ошибку проверки (обязательное поле) или создаст столбец db, который не принимает NULL.


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

1
Обновленный ответ здесь хорошо работает в 2016 году с Django 1.10 и использованием EmailField.
K0NG

4
Если вы обновляете a, CharFieldчтобы быть a CharNullField, вам нужно сделать это в три этапа. Сначала добавьте null=Trueв поле и перенесите это. Затем выполните миграцию данных, чтобы обновить все пустые значения, чтобы они были нулевыми. Наконец, конвертируйте поле в CharNullField. Если вы преобразуете поле перед миграцией данных, миграция данных ничего не изменит.
mlissner

3
Обратите внимание, что в обновленном решении from_db_value()не должно быть этого дополнительного contexпараметра. Должно бытьdef from_db_value(self, value, expression, connection):
Фил Гифорд

1
Комментарий от @PhilGyford применяется начиная с 2.0.
Шахид Хак

16

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

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

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

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


13

Быстрое решение состоит в том, чтобы сделать:

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

    if not self.bar:
        self.bar = None

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

2
знать, что использование MyModel.objects.bulk_create()обойдёт этот метод.
BenjaminGolder

Этот метод вызывается при сохранении из админ-панели? Я пытался, но это не так.
Кишан Мехта

1
@Kishan django-админ панель, к сожалению, пропустит эти хуки
Vincent Buscarello

@ E-Удовлетворяет ваша логика, поэтому я реализовал это, но ошибка все еще остается проблемой. Мне говорят, что ноль является дубликатом.
Винсент Бускарелло

6

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

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

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

Это не очень хорошее решение, так как вы создаете ненужные отношения.
Бурак Оздемир

3

Это исправлено теперь, когда https://code.djangoproject.com/ticket/4136 решен. В Django 1.11+ вы можете использовать models.CharField(unique=True, null=True, blank=True)без необходимости вручную конвертировать пустые значения в None.


1

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

1

Если у вас есть модель 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)

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


1

Вы можете добавить UniqueConstraintс условием nullable_field=nullи не включать это поле в fieldsсписок. Если вам нужно также ограничение с таким nullable_fieldзначением, которого нет null, вы можете добавить дополнительное.

Примечание: UniqueConstraint был добавлен начиная с django 2.2

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)
    
    class Meta:
        constraints = [
            # For bar == null only
            models.UniqueConstraint(fields=['name'], name='unique__name__when__bar__null',
                                    condition=Q(bar__isnull=True)),
            # For bar != null only
            models.UniqueConstraint(fields=['name', 'bar'], name='unique__name__when__bar__not_null')
        ]

Это работает! но я получаю исключение IntegrityError вместо ошибки проверки формы. Как вы справляетесь с этим? Поймать его и поднять ValidationError в представлениях create + update?
Гек

0

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

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


6
Это не правильный ответ. Смотрите этот ответ для объяснения .
Карл Дж

2
Согласен, это не правильно. Я только что протестировал IntegerField (blank = True, null = True, unique = True) в Django 1.4, и он допускает несколько строк с нулевыми значениями.
рабство
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.