Suponha que meu models.py seja assim:
class Character(models.Model):
name = models.CharField(max_length=255)
is_the_chosen_one = models.BooleanField()
Eu quero que apenas uma de minhas Character
instâncias tenha is_the_chosen_one == True
e todas as outras tenham is_the_chosen_one == False
. Qual a melhor forma de garantir que essa restrição de exclusividade seja respeitada?
Melhores notas para as respostas que levam em consideração a importância de respeitar as restrições nos níveis de banco de dados, modelo e formulário (admin)!
database
django
django-models
django-admin
django-forms
sampablokuper
fonte
fonte
through
tabelaManyToManyField
que precisa de umaunique_together
restrição.Respostas:
Sempre que preciso realizar essa tarefa, o que faço é substituir o método de salvamento do modelo e fazer com que ele verifique se algum outro modelo já tem o sinalizador definido (e desative-o).
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)
fonte
save(self)
para,save(self, *args, **kwargs)
mas a edição foi rejeitada. Algum dos revisores poderia levar algum tempo para explicar o porquê - já que isso parece ser consistente com as melhores práticas do Django.get()
inserir o objeto Character e depoissave()
inseri-lo novamente, você só precisa filtrar e atualizar, o que produz apenas uma consulta SQL e ajuda a manter o banco de dados consistente: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
que é importante aqui. Também é mais eficiente usando uma única consulta.Eu sobrescreveria o método save do modelo e se você definiu o booleano como True, certifique-se de que todos os outros estejam definidos como 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)
Tentei editar a resposta semelhante de Adam, mas foi rejeitada por alterar muito a resposta original. Desta forma é mais sucinta e eficiente, pois a verificação das demais entradas é feita em uma única consulta.
fonte
save
em uma@transaction.atomic
transação. Porque pode acontecer que você remova todas as bandeiras, mas então o salvamento falhe e você acabe com todos os personagens não escolhidos.@transaction.atomic
também protege contra condições de corrida.with transaction.atomic:
dentro da instrução if junto com salvar dentro do if. Em seguida, adicionando um bloco else e salvando também no bloco else.Em vez de usar limpeza / salvamento de modelo personalizado, criei um campo personalizado substituindo o
pre_save
método emdjango.db.models.BooleanField
. Em vez de gerar um erro se outro campo fosseTrue
, fiz todos os outros camposFalse
se fosseTrue
. Além disso, em vez de gerar um erro se o campo eraFalse
e nenhum outro eraTrue
, salvei o campo comoTrue
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)
fonte
Return True
parasetattr(model_instance, self.attname, True)
true
se você excluir a únicatrue
linha.A solução a seguir é um pouco feia, mas pode funcionar:
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)
Se você definir is_the_chosen_one como False ou None, será sempre NULL. Você pode ter NULL o quanto quiser, mas só pode ter um True.
fonte
Tentando pagar as contas com as respostas aqui, acho que alguns deles abordam o mesmo problema com sucesso e cada um é adequado em diferentes situações:
Eu escolheria:
@semente : Respeita a restrição nos níveis de banco de dados, modelo e formulário de administração, enquanto sobrescreve o Django ORM o mínimo possível. Além disso, pode
provavelmenteser usado dentro de umathrough
tabela deManyToManyField
umaunique_together
situação.(Vou verificar e relatar)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 : Acesse o banco de dados apenas uma vez extra e aceita a entrada atual como escolhida. Limpo e elegante.
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)
Outras soluções não adequadas para o meu caso, mas viáveis:
@nemocorp está substituindo o
clean
método para realizar uma validação. No entanto, ele não informa qual modelo é "aquele" e isso não é amigável. Apesar disso, é uma abordagem muito boa, especialmente se alguém não pretende ser tão agressivo quanto @Flyte.@ saul.shanabrook e @Thierry J. criariam um campo personalizado que alteraria qualquer outra entrada "is_the_one" para
False
ou geraria umValidationError
. Estou apenas relutante em implementar novos recursos na minha instalação do Django, a menos que seja absolutamente necessário.@daigorocub : Usa sinais Django. Acho que é uma abordagem única e dá uma dica de como usar Django Signals . No entanto, não tenho certeza se este é um uso - estritamente falando - "adequado" de sinais, uma vez que não posso considerar este procedimento como parte de uma "aplicação desacoplada".
fonte
save()
operação falhe!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
Você também pode usar o formulário acima para administrador, basta usar
class CharacterAdmin(admin.ModelAdmin): form = CharacterForm admin.site.register(Character, CharacterAdmin)
fonte
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")
Isso tornou a validação disponível no formulário administrativo básico
fonte
É mais simples adicionar este tipo de restrição ao seu modelo após o Django versão 2.2. Você pode usar diretamente
UniqueConstraint.condition
. Django DocsBasta substituir seus modelos
class Meta
desta forma:class Meta: constraints = [ UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one') ]
fonte
E isso é tudo.
def save(self, *args, **kwargs): if self.default_dp: DownloadPageOrder.objects.all().update(**{'default_dp': False}) super(DownloadPageOrder, self).save(*args, **kwargs)
fonte
Usando uma abordagem semelhante à de Saul, mas com um propósito ligeiramente diferente:
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
Esta implementação irá gerar um
ValidationError
ao tentar salvar outro registro com um valor True.Além disso, adicionei o
unique_for
argumento que pode ser definido como qualquer outro campo no modelo, para verificar a verdadeira exclusividade apenas para registros com o mesmo valor, como:class Phone(models.Model): user = models.ForeignKey(User) main = TrueUniqueBooleanField(unique_for='user', default=False)
fonte
Eu ganho pontos por responder à minha pergunta?
O problema era que ele estava no loop, corrigido por:
# 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()
fonte
Eu tentei algumas dessas soluções e acabei com outra, apenas por uma questão de abreviação de código (não precisa sobrescrever formulários ou salvar método). Para que isso funcione, o campo não pode ser único em sua definição, mas o sinal garante que isso aconteça.
# 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)
fonte
Atualização de 2020 para tornar as coisas menos complicadas para iniciantes:
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()
Obviamente, se você quiser que o booleano exclusivo seja False, basta trocar todas as instâncias de True por False e vice-versa.
fonte