Унікальні поля, які дозволяють отримати нулі в Django


135

У мене є модель Foo, у якій є смуга поля. Поле смуги повинно бути унікальним, але допускати в ньому нулі, тобто я хочу дозволити більше одного запису, якщо поле бар є 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-об’єктів, де бар є нульовим, це дає мені помилку: "Foo з цією смужкою вже існує".

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

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

Це працює просто чудово, це дозволяє мені вставити більше 1 запису, коли бар є нульовим, тому база даних дозволяє мені робити те, що я хочу, це просто щось не так з моделлю Django. Будь-які ідеї?

EDIT

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

Поки що я видалив унікальний специфікатор із властивості бар та керував унікальністю бар у додатку, проте все ще шукаю більш елегантне рішення. Будь-які рекомендації?


Я поки не можу коментувати, ось ось невелике доповнення до mayyhal: Оскільки 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 == "": #if Django намагається зберегти рядок '', надіслати db None (NULL) return Ніщо інше: return value #otherwise, just передайте значення
Єнс

Я відкрив для цього квиток на Джанго. Додайте підтримку. code.djangoproject.com/ticket/30210#ticket
Carl

Відповіді:


154

Джанго не вважає 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/…
користувач85461

62

** редагувати 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

Я думаю, що кращим способом, ніж перевизначення очищених_даних в адміністраторі, було б підкласинг 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
У разі оновлення CharFieldбути CharNullField, вам потрібно зробити , це в три етапи. Спочатку додайте null=Trueдо поля та перемістіть це. Потім зробіть міграцію даних, щоб оновити будь-які порожні значення, щоб вони були нульовими. Нарешті, перетворіть поле у ​​CharNullField. Якщо ви перетворите поле перед тим, як зробити міграцію даних, ваша міграція даних нічого не зробить.
mlissner

3
Зауважте, що в оновленому рішенні from_db_value()не повинно бути цього додаткового contexпараметра. Це повинно бутиdef from_db_value(self, value, expression, connection):
Філ Гіфорд

1
Коментар від @PhilGyford застосовується станом на 2.0.
Haque

16

Оскільки я новачок у стаковому потоці, я ще не можу відповідати на відповіді, але хотів би зазначити, що з філософської точки зору я не можу погодитися з найпопулярнішою відповіддю на це запитання. (автор Карен Трейсі)

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

Тому, щоб ваш код був справді OOP, ви повинні використовувати внутрішній метод вашої моделі 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()цього методу обійде.
БенджамінГолдер

Чи називається цей метод, коли ми економимо з панелі адміністратора? Я спробував, але це не.
Кішань Мехта

1
@Kishan django-admin панель, на жаль, пропустить ці гачки
Vincent Buscarello

@ e-satis Ваша логіка звучить, тому я реалізував це, але помилка все ще є проблемою. Мені, як сказати, null є дублікат.
Вінсент Бускарелло

6

Ще одне можливе рішення

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

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

Це не гарне рішення, оскільки ви створюєте непотрібні стосунки.
Бурак Оздемір


1

Нещодавно у мене була така ж вимога. Замість того, щоб підкласифікувати різні поля, я вирішив замінити метод збереження () на своїй моделі (названий нижче "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 був Null або унікальним, ви можете змінити метод збереження моделі:

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 у створенні + оновленнях переглядів?
gek

0

На краще чи гірше, Джанго вважає NULLрівнозначним NULLцілям перевірки унікальності. Насправді не обійтися, як написати власну реалізацію перевірки унікальності, яка враховуєNULL унікальною незалежно від того, скільки разів вона зустрічається в таблиці.

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


6
Це не правильна відповідь. Дивіться цю відповідь для пояснення .
Карл Г

2
Погодився, це неправильно. Щойно я тестував IntegerField (порожній = True, null = True, унікальний = True) у Django 1.4, і він дозволяє декілька рядків з нульовими значеннями.
шибениця
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.