Supposons que mon models.py soit comme ceci:
class Character(models.Model):
name = models.CharField(max_length=255)
is_the_chosen_one = models.BooleanField()
Je veux qu'une seule de mes Character
instances ait is_the_chosen_one == True
et que toutes les autres aient is_the_chosen_one == False
. Comment puis-je m'assurer au mieux que cette contrainte d'unicité est respectée?
Meilleures notes pour les réponses qui prennent en compte l'importance de respecter la contrainte au niveau de la base de données, du modèle et du formulaire (admin)!
database
django
django-models
django-admin
django-forms
sampablokuper
la source
la source
through
tableManyToManyField
qui nécessite uneunique_together
contrainte.Réponses:
Chaque fois que j'ai eu besoin d'accomplir cette tâche, ce que j'ai fait est de remplacer la méthode de sauvegarde du modèle et de la faire vérifier si un autre modèle a déjà défini l'indicateur (et de le désactiver).
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)
la source
save(self)
ensave(self, *args, **kwargs)
mais la modification a été rejetée. L'un des examinateurs pourrait-il prendre le temps d'expliquer pourquoi - car cela semble être conforme aux meilleures pratiques de Django.get()
ingérer l'objet Character puis de lesave()
réintégrer, il vous suffit de filtrer et de mettre à jour, ce qui ne produit qu'une seule requête SQL et aide à garder la base de données cohérente: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
ce qui est important ici. Il est également plus efficace d'utiliser une seule requête.Je remplacerais la méthode de sauvegarde du modèle et si vous avez défini le booléen sur True, assurez-vous que tous les autres sont définis sur 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)
J'ai essayé de modifier la réponse similaire d'Adam, mais elle a été rejetée pour avoir trop changé la réponse originale. Cette méthode est plus succincte et efficace car la vérification des autres entrées se fait en une seule requête.
la source
save
une@transaction.atomic
transaction. Parce qu'il peut arriver que vous supprimiez tous les indicateurs, mais que l'enregistrement échoue et que vous vous retrouviez avec tous les caractères non choisis.@transaction.atomic
protège également des conditions de course.with transaction.atomic:
à l'intérieur de l'instruction if avec l'enregistrement à l'intérieur du if. Ensuite, ajoutez un bloc else et enregistrez également dans le bloc else.Au lieu d'utiliser le nettoyage / l'enregistrement du modèle personnalisé, j'ai créé un champ personnalisé remplaçant la
pre_save
méthode activéedjango.db.models.BooleanField
. Au lieu de déclencher une erreur si un autre champ l'étaitTrue
, j'ai créé tous les autres champsFalse
si c'était le casTrue
. Aussi au lieu de générer une erreur si le champ étaitFalse
et aucun autre champ ne l'étaitTrue
, je l'ai enregistré le champTrue
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)
la source
Return True
àsetattr(model_instance, self.attname, True)
true
si vous supprimiez la seuletrue
ligne.La solution suivante est un peu moche mais pourrait fonctionner:
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)
Si vous définissez is_the_chosen_one sur False ou None, il sera toujours NULL. Vous pouvez avoir NULL autant que vous le souhaitez, mais vous ne pouvez avoir qu'un seul True.
la source
En essayant de joindre les deux bouts avec les réponses ici, je trouve que certains d'entre eux résolvent le même problème avec succès et chacun convient à différentes situations:
Je choisirais:
@semente : respecte la contrainte au niveau de la base de données, du modèle et du formulaire d'administration tout en écrasant le moins possible Django ORM. De plus, il peut
Probablementêtre utilisé à l'intérieur d'unethrough
table d'unManyToManyField
dans ununique_together
situation.(Je vais le vérifier et faire un rapport)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 : n'atteint la base de données qu'une seule fois et accepte l'entrée actuelle comme celle choisie. Propre et élégant.
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)
Autres solutions non adaptées à mon cas mais viables:
@nemocorp remplace la
clean
méthode pour effectuer une validation. Cependant, il n'indique pas quel modèle est «celui» et ce n'est pas convivial. Malgré cela, c'est une très belle approche surtout si quelqu'un n'a pas l'intention d'être aussi agressif que @Flyte.@ saul.shanabrook et @Thierry J. créeraient un champ personnalisé qui changerait toute autre entrée "is_the_one" en
False
ou augmenterait unValidationError
. Je suis juste réticent à intégrer de nouvelles fonctionnalités à mon installation Django à moins que cela ne soit absolument nécessaire.@daigorocub : utilise les signaux Django. Je trouve que c'est une approche unique et donne un indice sur la façon d'utiliser Django Signals . Cependant, je ne suis pas sûr qu'il s'agisse d'une utilisation - strictement parlant - des signaux, car je ne peux pas considérer cette procédure comme faisant partie d'une «application découplée».
la source
save()
échec de l' opération!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
Vous pouvez également utiliser le formulaire ci-dessus pour l'administrateur, utilisez simplement
class CharacterAdmin(admin.ModelAdmin): form = CharacterForm admin.site.register(Character, CharacterAdmin)
la source
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")
Cela a rendu la validation disponible dans le formulaire d'administration de base
la source
Il est plus simple d'ajouter ce type de contrainte à votre modèle après Django version 2.2. Vous pouvez utiliser directement
UniqueConstraint.condition
. Documents DjangoRemplacez simplement vos modèles
class Meta
comme ceci:class Meta: constraints = [ UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one') ]
la source
Et c'est tout.
def save(self, *args, **kwargs): if self.default_dp: DownloadPageOrder.objects.all().update(**{'default_dp': False}) super(DownloadPageOrder, self).save(*args, **kwargs)
la source
En utilisant une approche similaire à Saul, mais un objectif légèrement différent:
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
Cette implémentation lèvera un
ValidationError
lors de la tentative de sauvegarde d'un autre enregistrement avec une valeur True.En outre, j'ai ajouté l'
unique_for
argument qui peut être défini sur n'importe quel autre champ du modèle, pour vérifier l'unicité vraie uniquement pour les enregistrements avec la même valeur, tels que:class Phone(models.Model): user = models.ForeignKey(User) main = TrueUniqueBooleanField(unique_for='user', default=False)
la source
Est-ce que j'obtiens des points pour répondre à ma question?
Le problème était qu'il se retrouvait dans la boucle, corrigé par:
# 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()
la source
J'ai essayé certaines de ces solutions et j'ai fini avec une autre, juste pour des raisons de brièveté du code (pas besoin de remplacer les formulaires ou d'enregistrer la méthode). Pour que cela fonctionne, le champ ne peut pas être unique dans sa définition, mais le signal garantit que cela se produit.
# 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)
la source
Mise à jour 2020 pour rendre les choses moins compliquées pour les débutants:
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()
Bien sûr, si vous voulez que le booléen unique soit False, vous échangeriez simplement chaque instance de True avec False et vice versa.
la source