Унікальне значення BooleanField у Django?


87

Припустимо, мій models.py виглядає так:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

Я хочу, Characterщоб мав лише один із моїх випадків, is_the_chosen_one == Trueа всі інші is_the_chosen_one == False. Як я можу найкраще забезпечити дотримання цього обмеження унікальності?

Найкращі оцінки відповідей, які враховують важливість дотримання обмежень на рівні бази даних, моделі та форми (адміністратора)!


4
Хороше питання. Мені також цікаво, чи можливо встановити таке обмеження. Я знаю, що якщо ви просто зробили це унікальне обмеження, у вашій базі даних з’явиться лише два можливих рядки ;-)
Андре Міллер,

Необов’язково: якщо ви використовуєте NullBooleanField, ви повинні мати: (True, False, будь-яку кількість NULL).
Метью Шінкель, 02

Згідно з моїм дослідженням , @semente відповідь враховує важливість дотримання обмежень на рівні бази даних, моделі та форми (адміністратора), в той час як він забезпечує чудове рішення навіть для throughтаблиці, ManyToManyFieldяка потребує unique_togetherобмеження.
raratiru

Відповіді:


66

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

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            try:
                temp = Character.objects.get(is_the_chosen_one=True)
                if self != temp:
                    temp.is_the_chosen_one = False
                    temp.save()
            except Character.DoesNotExist:
                pass
        super(Character, self).save(*args, **kwargs)

3
Я б просто змінив 'def save (self):' на: 'def save (self, * args, ** kwargs):'
Marek

8
Я намагався змінити це, щоб змінити save(self)на, save(self, *args, **kwargs)але редагування було відхилено. Чи може хтось із рецензентів витратити час, щоб пояснити, чому - оскільки це, здається, відповідає найкращій практиці Django.
scytale

14
Я спробував редагувати, щоб усунути потребу в спробі / за винятком і зробити процес більш ефективним, але його було відхилено .. Замість того, get()щоб вводити об'єкт "Символ", а потім save()вводити його знову, вам просто потрібно відфільтрувати та оновити, що створює лише один запит SQL і допомагає підтримувати послідовність БД: if self.is_the_chosen_one:<newline> Character.objects.filter(is_the_chosen_one=True).update(is_the_chosen_one=False)<newline>super(Character, self).save(*args, **kwargs)
Елліс Персіваль,

2
Я не можу запропонувати кращого методу для виконання цього завдання, але я хочу сказати, що ніколи не довіряйте методам збереження чи очищення, якщо у вас запущено веб-додаток, яке може перенести кілька запитів до кінцевої точки в той самий момент. Ви все ще повинні впровадити більш безпечний спосіб, можливо, на рівні бази даних.
u.unver34

1
Нижче є краща відповідь. Відповідь Елліса Персіваля використовує те, transaction.atomicщо тут важливо. Це також ефективніше за допомогою одного запиту.
alexbhandari

33

Я б замінив метод збереження моделі, і якщо ви встановили для логічного значення значення True, переконайтеся, що для всіх інших встановлено значення False.

from django.db import transaction

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def save(self, *args, **kwargs):
        if not self.is_the_chosen_one:
            return super(Character, self).save(*args, **kwargs)
        with transaction.atomic():
            Character.objects.filter(
                is_the_chosen_one=True).update(is_the_chosen_one=False)
            return super(Character, self).save(*args, **kwargs)

Я спробував відредагувати подібну відповідь Адама, але вона була відхилена через занадто багато змін до початкової відповіді. Цей спосіб є більш стислим та ефективним, оскільки перевірка інших записів здійснюється в одному запиті.


7
Я думаю , що це найкраща відповідь, але я хотів би запропонувати упаковку saveв @transaction.atomicтранзакцію. Тому що може трапитися так, що ви видалите всі прапори, але потім збереження не вдається, і в результаті ви отримуєте не вибрані символи.
Мітар

Дякую, що так сказали. Ви абсолютно праві, і я оновлю відповідь.
Елліс Персіваль

@Mitar @transaction.atomicтакож захищає від перегонів.
Павел Фурманіяк,

1
Найкраще рішення серед усіх!
Артуро

1
Щодо транзакції. Atomic, я використовував менеджер контекстів замість декоратора. Я не бачу підстав використовувати атомні транзакції для кожної моделі, окрім збереження, оскільки це має значення лише у випадку, якщо логічне поле є істинним. Я пропоную використовувати with transaction.atomic:всередині оператора if разом із збереженням усередині if. Потім додаємо блок else, а також зберігаємо в блоці else.
alexbhandari

29

Замість того, щоб використовувати очищення / збереження власної моделі, я створив спеціальне поле, яке перевизначає pre_saveметод django.db.models.BooleanField. Замість того, щоб викликати помилку, якщо було інше поле True, я створив усі інші поля, Falseякщо воно було True. Крім того, замість того, щоб викликати помилку, якщо поле було Falseі жодного іншого поля не було True, я зберег це поле якTrue

fields.py

from django.db.models import BooleanField


class UniqueBooleanField(BooleanField):
    def pre_save(self, model_instance, add):
        objects = model_instance.__class__.objects
        # If True then set all others as False
        if getattr(model_instance, self.attname):
            objects.update(**{self.attname: False})
        # If no true object exists that isnt saved model, save as True
        elif not objects.exclude(id=model_instance.id)\
                        .filter(**{self.attname: True}):
            return True
        return getattr(model_instance, self.attname)

# To use with South
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^project\.apps\.fields\.UniqueBooleanField"])

models.py

from django.db import models

from project.apps.fields import UniqueBooleanField


class UniqueBooleanModel(models.Model):
    unique_boolean = UniqueBooleanField()

    def __unicode__(self):
        return str(self.unique_boolean)

2
Це виглядає набагато чистішим за інші методи
фісташка

2
Мені також подобається це рішення, хоча здається потенційно небезпечним наявність об'єктів. Update для всіх інших об'єктів значення False у випадку, коли моделі UniqueBoolean є True. Було б навіть краще, якби UniqueBooleanField взяв необов’язковий аргумент, щоб вказати, чи слід для інших об’єктів встановити значення False, чи слід викликати помилку (інша розумна альтернатива). Крім того, враховуючи ваш коментар у elif, де ви хочете встановити атрибут true, я думаю, вам слід змінити Return Trueнаsetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanField насправді не є унікальним, оскільки ви можете мати скільки завгодно помилкових значень. Не знаєте, що було б кращою назвою ... OneTrueBooleanField? Що я справді хочу, це мати можливість застосувати це в поєднанні із зовнішнім ключем, щоб я міг мати BooleanField, якому було дозволено бути Істинним лише раз на відносини (наприклад, CreditCard має "основне" поле та FK для користувача та комбінація Користувач / Основна - Істинно один раз за використання). У такому випадку я думаю, що відповідь Адама, яка перевершує збереження, буде для мене більш простою.
Ендрю Чейз,

1
Слід зазначити, що цей метод дозволяє вам опинитися в стані без встановлених рядків, як trueякщо б ви видалили єдиний trueрядок.
rblk

11

Наступне рішення трохи потворне, але може працювати:

class MyModel(models.Model):
    is_the_chosen_one = models.NullBooleanField(default=None, unique=True)

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one is False:
            self.is_the_chosen_one = None
        super(MyModel, self).save(*args, **kwargs)

Якщо ви встановите для is_the_chosen_one значення False або None, це завжди буде NULL. Ви можете мати NULL скільки завгодно, але ви можете мати лише один True.


1
Перше рішення, про яке я також думав. NULL завжди унікальний, тому у вас завжди може бути стовпець з кількома NULL.
kaleissin

10

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

Я б вибрав:

  • @semente : Поважає обмеження на рівні бази даних, моделі та форми адміністратора, в той час як воно перекриває Django ORM якнайменше. Більше того, це можеймовірноможна використовувати всередині throughтаблиці ManyToManyFieldв unique_togetherситуації.(Я перевірю це і повідомлю)

    class MyModel(models.Model):
        is_the_chosen_one = models.NullBooleanField(default=None, unique=True)
    
        def save(self, *args, **kwargs):
            if self.is_the_chosen_one is False:
                self.is_the_chosen_one = None
            super(MyModel, self).save(*args, **kwargs)
    
  • @Ellis Percival : Переглядає базу даних лише один додатковий час і приймає поточний запис як вибраний. Чисто та елегантно.

    from django.db import transaction
    
    class Character(models.Model):
        name = models.CharField(max_length=255)
        is_the_chosen_one = models.BooleanField()
    
    def save(self, *args, **kwargs):
        if not self.is_the_chosen_one:
            # The use of return is explained in the comments
            return super(Character, self).save(*args, **kwargs)  
        with transaction.atomic():
            Character.objects.filter(
                is_the_chosen_one=True).update(is_the_chosen_one=False)
            # The use of return is explained in the comments
            return super(Character, self).save(*args, **kwargs)  
    

Інші рішення, які не підходять для мого випадку, але життєздатні:

@nemocorp перевизначає cleanметод для перевірки. Однак він не повідомляє, яка модель є "тією", і це не зручно для користувачів. Незважаючи на це, це дуже приємний підхід, особливо якщо хтось не збирається бути таким агресивним, як @Flyte.

@ saul.shanabrook та @Thierry J. створили б власне поле, яке або змінило б будь-який інший запис "is_the_one", Falseабо зробило б ValidationError. Я просто не хочу вводити нові функції для моєї інсталяції Django, якщо це абсолютно не потрібно.

@daigorocub : використовує сигнали Django. Я вважаю це унікальним підходом і дає підказку про те, як використовувати сигнали Django . Однак я не впевнений, чи це - строго кажучи - "правильне" використання сигналів, оскільки я не можу розглядати цю процедуру як частину "роз'єднаної програми".


Дякуємо за огляд! Я трохи оновив свою відповідь, виходячи з одного з коментарів, на випадок, якщо ви також захочете оновити свій код.
Елліс Персіваль

@EllisPercival Дякую за підказку! Я відповідно оновив код. Майте на увазі, що models.Model.save () щось не повертає.
raratiru

Це чудово. В основному це лише для того, щоб заощадити, маючи перший прибуток на власній лінії. Ваша версія насправді неправильна, оскільки вона не включає .save () в атомну транзакцію. Плюс, замість цього має бути 'withaction.atomic ():'.
Елліс Персіваль,

1
@EllisPercival Добре, дякую! Дійсно, нам потрібно все відкотити назад, якщо save()операція не вдасться!
raratiru

6
class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def save(self, *args, **kwargs):
        if self.is_the_chosen_one:
            qs = Character.objects.filter(is_the_chosen_one=True)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            if qs.count() != 0:
                # choose ONE of the next two lines
                self.is_the_chosen_one = False # keep the existing "chosen one"
                #qs.update(is_the_chosen_one=False) # make this obj "the chosen one"
        super(Character, self).save(*args, **kwargs)

class CharacterForm(forms.ModelForm):
    class Meta:
        model = Character

    # if you want to use the new obj as the chosen one and remove others, then
    # be sure to use the second line in the model save() above and DO NOT USE
    # the following clean method
    def clean_is_the_chosen_one(self):
        chosen = self.cleaned_data.get('is_the_chosen_one')
        if chosen:
            qs = Character.objects.filter(is_the_chosen_one=True)
            if self.instance.pk:
                qs = qs.exclude(pk=self.instance.pk)
            if qs.count() != 0:
                raise forms.ValidationError("A Chosen One already exists! You will pay for your insolence!")
        return chosen

Ви також можете скористатися наведеною вище формою для адміністратора, просто використовуйте

class CharacterAdmin(admin.ModelAdmin):
    form = CharacterForm
admin.site.register(Character, CharacterAdmin)

4
class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

    def clean(self):
        from django.core.exceptions import ValidationError
        c = Character.objects.filter(is_the_chosen_one__exact=True)  
        if c and self.is_the_chosen:
            raise ValidationError("The chosen one is already here! Too late")

Це зробило перевірку доступною у базовій формі адміністратора


4

Додати такий тип обмежень до вашої моделі простіше після Django версії 2.2. Ви можете безпосередньо використовувати UniqueConstraint.condition. Документи Django

Просто перевизначте свої моделі class Metaтаким чином:

class Meta:
    constraints = [
        UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one')
    ]

2

І це все.

def save(self, *args, **kwargs):
    if self.default_dp:
        DownloadPageOrder.objects.all().update(**{'default_dp': False})
    super(DownloadPageOrder, self).save(*args, **kwargs)

2

Використовуючи подібний підхід, як Саул, але дещо іншої мети:

class TrueUniqueBooleanField(BooleanField):

    def __init__(self, unique_for=None, *args, **kwargs):
        self.unique_for = unique_for
        super(BooleanField, self).__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        value = super(TrueUniqueBooleanField, self).pre_save(model_instance, add)

        objects = model_instance.__class__.objects

        if self.unique_for:
            objects = objects.filter(**{self.unique_for: getattr(model_instance, self.unique_for)})

        if value and objects.exclude(id=model_instance.id).filter(**{self.attname: True}):
            msg = 'Only one instance of {} can have its field {} set to True'.format(model_instance.__class__, self.attname)
            if self.unique_for:
                msg += ' for each different {}'.format(self.unique_for)
            raise ValidationError(msg)

        return value

Ця реалізація підніме a ValidationErrorпри спробі зберегти інший запис зі значенням True.

Крім того, я додав unique_forаргумент, який можна встановити в будь-яке інше поле моделі, щоб перевірити справжню унікальність лише для записів з однаковим значенням, таких як:

class Phone(models.Model):
    user = models.ForeignKey(User)
    main = TrueUniqueBooleanField(unique_for='user', default=False)

1

Чи отримую я бали за відповідь на своє запитання?

проблема полягала в тому, що він опинився в циклі, виправлений:

    # is this the testimonial image, if so, unselect other images
    if self.testimonial_image is True:
        others = Photograph.objects.filter(project=self.project).filter(testimonial_image=True)
        pdb.set_trace()
        for o in others:
            if o != self: ### important line
                o.testimonial_image = False
                o.save()

Ні, немає балів для відповіді на власне запитання та прийняття цієї відповіді. Однак є деякі зауваження, якщо хтось проголосує за вашу відповідь. :)
dandan78

Ви впевнені, що не хотіли відповісти тут на власне запитання ? В основному у вас і @sampablokuper було одне і те ж питання
j_syk

1

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

# making default_number True unique
@receiver(post_save, sender=Character)
def unique_is_the_chosen_one(sender, instance, **kwargs):
    if instance.is_the_chosen_one:
        Character.objects.all().exclude(pk=instance.pk).update(is_the_chosen_one=False)

0

Оновлення 2020 року, щоб зробити речі менш складними для початківців:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField(blank=False, null=False, default=False)

    def save(self):
         if self.is_the_chosen_one == True:
              items = Character.objects.filter(is_the_chosen_one = True)
              for x in items:
                   x.is_the_chosen_one = False
                   x.save()
         super().save()

Звичайно, якщо ви хочете, щоб унікальний логічний тип був False, ви просто поміняєте кожен екземпляр True на False і навпаки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.