Предположим, мой 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
. Как я могу наилучшим образом обеспечить соблюдение этого ограничения уникальности?
Высшие оценки за ответы, учитывающие важность соблюдения ограничений на уровне базы данных, модели и (администратора) формы!
database
django
django-models
django-admin
django-forms
сампаблокупер
источник
источник
through
таблицы, вManyToManyField
которой требуетсяunique_together
ограничение.Ответы:
Всякий раз, когда мне нужно было выполнить эту задачу, я переопределял метод сохранения для модели и проверял, установлен ли флаг у какой-либо другой модели (и выключил его).
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)
источник
save(self)
на,save(self, *args, **kwargs)
но изменение было отклонено. Не мог бы кто-нибудь из рецензентов найти время, чтобы объяснить, почему - поскольку это, казалось бы, соответствует передовой практике Django.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)
transaction.atomic
что здесь важно. Также более эффективно использовать один запрос.Я бы переопределил метод сохранения модели, и если вы установили логическое значение 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)
Я попытался отредактировать аналогичный ответ Адама, но он был отклонен из-за слишком большого изменения исходного ответа. Этот способ более краток и эффективен, поскольку проверка других записей выполняется в одном запросе.
источник
save
в@transaction.atomic
транзакцию. Потому что может случиться так, что вы удалите все флаги, но тогда сохранение не удастся, и вы останетесь без выбора всех символов.@transaction.atomic
также защищает от состояния гонки.with transaction.atomic:
внутри оператора if вместе с сохранением внутри if. Затем добавляем блок else, а также сохраняем в блоке else.Вместо использования пользовательской очистки / сохранения модели я создал настраиваемое поле, переопределив
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)
источник
Return True
наsetattr(model_instance, self.attname, True)
true
если бы вы удалили единственнуюtrue
строку.Следующее решение немного некрасиво, но может сработать:
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.
источник
Пытаясь свести концы с концами с ответами здесь, я обнаружил, что некоторые из них успешно решают одну и ту же проблему, и каждый из них подходит в разных ситуациях:
Я бы выбрал:
@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 . Однако я не уверен, является ли это, строго говоря, «правильным» использованием сигналов, поскольку я не могу рассматривать эту процедуру как часть «развязанного приложения».
источник
save()
операция не удалась!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)
источник
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")
Это сделало проверку доступной в основной форме администратора.
источник
После 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') ]
источник
И это все.
def save(self, *args, **kwargs): if self.default_dp: DownloadPageOrder.objects.all().update(**{'default_dp': False}) super(DownloadPageOrder, self).save(*args, **kwargs)
источник
Используя тот же подход, что и у Саула, но немного с другой целью:
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)
источник
Получу ли я баллы за ответ на свой вопрос?
проблема заключалась в том, что он оказался в цикле, исправленный:
# 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()
источник
Я пробовал некоторые из этих решений и в итоге нашел другое, просто ради краткости кода (не нужно переопределять формы или метод сохранения). Чтобы это работало, поле не может быть уникальным в своем определении, но сигнал гарантирует, что это произойдет.
# 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)
источник
Обновление 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 и наоборот.
источник