Уникальное значение BooleanField в Django?
Предположим, что мои 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
. Как я могу наилучшим образом обеспечить соблюдение этого ограничения уникальности?
Лучшие оценки ответов, которые учитывают важность соблюдения ограничений на уровнях формы базы данных, модели и (администратора)!
Ответы
Ответ 1
Всякий раз, когда мне нужно было выполнить эту задачу, то, что я сделал, переопределяет метод сохранения для модели и проверяет, установлена ли какая-либо другая модель уже установленным флагом (и отключите ее).
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)
Ответ 2
Я бы переопределил метод сохранения модели, и если вы установили логическое значение 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)
Я попытался отредактировать аналогичный ответ адама, но он был отклонен из-за слишком большого изменения исходного ответа. Этот способ является более кратким и эффективным, поскольку проверка других записей выполняется в одном запросе.
Ответ 3
Вместо использования пользовательской очистки/сохранения пользовательской модели я создал настраиваемое поле, переопределяющее метод 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)
Ответ 4
Следующее решение немного уродливое, но может работать:
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.
Ответ 5
Пытаясь свести концы с концами с ответами здесь, я обнаружил, что некоторые из них успешно решают одну и ту же проблему, и каждый подходит в различных ситуациях:
Я бы выбрал:
@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. Однако я не уверен, является ли это -strictly speaking- "правильным" использованием сигналов, поскольку я не могу рассматривать эту процедуру как часть "несвязанного приложения".
Ответ 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)
Ответ 7
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")
Это сделало проверку доступной в основной форме администратора
Ответ 8
И все.
def save(self, *args, **kwargs):
if self.default_dp:
DownloadPageOrder.objects.all().update(**{'default_dp': False})
super(DownloadPageOrder, self).save(*args, **kwargs)
Ответ 9
Используя подобный подход, как Saul, но немного другое назначение:
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)
Ответ 10
Получаю ли я ответы на свой вопрос?
проблема заключалась в том, что она находилась в цикле, исправлена:
# 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()
Ответ 11
Я попробовал некоторые из этих решений, и в итоге получил еще один, только ради короткого кода (не нужно переопределять формы или сохранять метод).
Для этого поле не может быть уникальным в этом определении, но сигнал гарантирует, что это произойдет.
# 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)