Уникальное значение BooleanField в Django?

89

Предположим, мой 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, тогда у вас должно быть: (Истина, Ложь, любое количество НУЛЕВОЙ).
Мэтью Шинкель
Согласно моему исследованию , ответ @semente учитывает важность соблюдения ограничений на уровнях базы данных, модели и (админки) формы, в то время как он обеспечивает отличное решение даже для throughтаблицы, в ManyToManyFieldкоторой требуется unique_togetherограничение.
раратиру 07

Ответы:

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):'
Марек
8
Я попытался отредактировать это, чтобы изменить save(self)на, save(self, *args, **kwargs)но изменение было отклонено. Не мог бы кто-нибудь из рецензентов найти время, чтобы объяснить, почему - поскольку это, казалось бы, соответствует передовой практике Django.
scytale
14
Я пробовал редактировать, чтобы убрать необходимость в try / except и сделать процесс более эффективным, но он был отклонен. Вместо того, get()чтобы вставить объект Character и затем 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
35

Я бы переопределил метод сохранения модели, и если вы установили логическое значение 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)

Я попытался отредактировать аналогичный ответ Адама, но он был отклонен из-за слишком большого изменения исходного ответа. Этот способ более краток и эффективен, поскольку проверка других записей выполняется в одном запросе.

Эллис Персиваль
источник
8
Думаю, это лучший ответ, но я бы посоветовал обернуть save в @transaction.atomicтранзакцию. Потому что может случиться так, что вы удалите все флаги, но тогда сохранение не удастся, и вы останетесь без выбора всех символов.
Mitar
Спасибо, что так сказали. Вы абсолютно правы, я обновлю ответ.
Эллис Персиваль,
@Mitar @transaction.atomicтакже защищает от состояния гонки.
Павел Фурманяк
1
Лучшее решение среди всех!
Артуро
1
Что касается transaction.atomic, я использовал диспетчер контекста вместо декоратора. Я не вижу причин использовать атомарную транзакцию для каждой сохраненной модели, поскольку это имеет значение только в том случае, если логическое поле истинно. Я предлагаю использовать with transaction.atomic:внутри оператора if вместе с сохранением внутри if. Затем добавляем блок else, а также сохраняем в блоке else.
Алексбхандари
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)
Saul.shanabrook
источник
2
Это выглядит намного более чистым, чем другие методы
фисташ
2
Мне также нравится это решение, хотя кажется потенциально опасным, чтобы объект objects.update устанавливал для всех других объектов значение False в случае, когда для модели UniqueBoolean установлено значение True. Было бы даже лучше, если бы UniqueBooleanField принимал необязательный аргумент, чтобы указать, следует ли установить для других объектов значение False или должна возникать ошибка (другая разумная альтернатива). Кроме того, учитывая ваш комментарий в elif, где вы хотите установить для атрибута значение true, я думаю, вам следует изменить его Return Trueнаsetattr(model_instance, self.attname, True)
Эндрю Чейз
2
UniqueBooleanField на самом деле не уникален, поскольку вы можете иметь столько значений False, сколько хотите. Не уверен, что было бы лучше ... OneTrueBooleanField? Что я действительно хочу, так это иметь возможность охватывать это в сочетании с внешним ключом, чтобы у меня было BooleanField, которому разрешено быть True только один раз для каждого отношения (например, 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 08
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 Signals . Однако я не уверен, является ли это, строго говоря, «правильным» использованием сигналов, поскольку я не могу рассматривать эту процедуру как часть «развязанного приложения».

раратиру
источник
Спасибо за обзор! Я немного обновил свой ответ, основываясь на одном из комментариев, на случай, если вы тоже захотите обновить свой код здесь.
Эллис Персиваль
@EllisPercival Спасибо за подсказку! Я обновил код соответственно. Имейте в виду, что models.Model.save () ничего не возвращает.
раратиру
Хорошо. В основном, чтобы сэкономить, имея первую отдачу по собственной линии. Ваша версия на самом деле неверна, поскольку она не включает .save () в атомарную транзакцию. Кроме того, вместо этого должно быть указано «with transaction.atomic ():».
Эллис Персиваль
1
@EllisPercival Хорошо, спасибо! Действительно, нам нужен откат всего, если save()операция не удалась!
раратиру
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)
Shadfc
источник
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)
Palestamp
источник
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

Эта реализация вызовет ошибку 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()
Bytejunkie
источник
Нет, нет очков за ответ на свой вопрос и принятие этого ответа. Однако есть моменты, которые следует отметить, если кто-то проголосует за ваш ответ. :)
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 и наоборот.

Джей
источник