Eindeutiger BooleanField-Wert in Django?

86

Angenommen, meine models.py ist wie folgt:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

Ich möchte nur eine meiner CharacterInstanzen is_the_chosen_one == Trueund alle anderen haben is_the_chosen_one == False. Wie kann ich am besten sicherstellen, dass diese Eindeutigkeitsbeschränkung eingehalten wird?

Bestnoten für Antworten, die die Wichtigkeit der Einhaltung der Einschränkungen auf Datenbank-, Modell- und (Administrator-) Formularebene berücksichtigen!

sampablokuper
quelle
4
Gute Frage. Ich bin auch neugierig, ob es möglich ist, eine solche Einschränkung einzurichten. Ich weiß, wenn Sie es einfach zu einer eindeutigen Einschränkung gemacht haben, werden Sie nur zwei mögliche Zeilen in Ihrer Datenbank haben ;-)
Andre Miller
Nicht unbedingt: Wenn Sie ein NullBooleanField verwenden, sollten Sie in der Lage sein: (ein True, ein False, eine beliebige Anzahl von NULL-Werten).
Matthew Schinckel
Nach meiner Forschung , @semente Antwort, berücksichtigt die Bedeutung der Beschränkung bei der Datenbank - Modell zu achten und (admin) Formularebenen , während es eine große Lösung sogar für einen bietet throughTabelle , ManyToManyFielddass eine braucht unique_togetherEinschränkung.
Raratiru

Antworten:

64

Wann immer ich diese Aufgabe ausführen musste, habe ich die Speichermethode für das Modell überschrieben und prüfen lassen, ob für ein anderes Modell das Flag bereits gesetzt ist (und es deaktivieren).

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)
Adam
quelle
3
Ich würde einfach 'def save (self):' in: 'def save (self, * args, ** kwargs):'
Marek
8
Ich habe versucht, dies zu bearbeiten, um es zu ändern save(self), save(self, *args, **kwargs)aber die Bearbeitung wurde abgelehnt. Könnte sich einer der Rezensenten Zeit nehmen, um zu erklären, warum - da dies mit der Best Practice von Django in Einklang zu stehen scheint.
Sense
13
Ich habe versucht zu bearbeiten, um die Notwendigkeit von try / exception zu beseitigen und den Prozess effizienter zu gestalten, aber es wurde abgelehnt. Anstatt get()das Character-Objekt zu bearbeiten und es dann erneut zu bearbeiten, müssen save()Sie nur filtern und aktualisieren, wodurch nur eine SQL-Abfrage erstellt wird und hilft, die DB konsistent zu halten: 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)
Ellis Percival
2
Ich kann keine bessere Methode vorschlagen, um diese Aufgabe zu erfüllen, aber ich möchte sagen, dass Sie niemals sicheren oder sauberen Methoden vertrauen, wenn Sie eine Webanwendung ausführen, bei der Sie möglicherweise einige Anfragen gleichzeitig an einen Endpunkt senden. Sie müssen möglicherweise noch einen sichereren Weg implementieren, möglicherweise auf Datenbankebene.
u.unver34
1
Es gibt unten eine bessere Antwort. Ellis Percivals Antwort verwendet, transaction.atomicwas hier wichtig ist. Es ist auch effizienter, eine einzelne Abfrage zu verwenden.
Alexbhandari
32

Ich würde die Speichermethode des Modells überschreiben. Wenn Sie den Booleschen Wert auf True gesetzt haben, stellen Sie sicher, dass alle anderen auf False gesetzt sind.

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)

Ich habe versucht, die ähnliche Antwort von Adam zu bearbeiten, aber sie wurde abgelehnt, weil zu viel von der ursprünglichen Antwort geändert wurde. Diese Methode ist prägnanter und effizienter, da die Überprüfung anderer Einträge in einer einzigen Abfrage erfolgt.

Ellis Percival
quelle
7
Ich denke, dies ist die beste Antwort, aber ich würde vorschlagen, saveeine @transaction.atomicTransaktion abzuschließen. Weil es passieren kann, dass Sie alle Flags entfernen, das Speichern dann jedoch fehlschlägt und alle Zeichen nicht ausgewählt werden.
Mitar
Danke, dass Sie das sagen. Sie haben absolut Recht und ich werde die Antwort aktualisieren.
Ellis Percival
@Mitar @transaction.atomicschützt auch vor Rennbedingungen.
Pawel Furmaniak
1
Beste Lösung unter allen!
Arturo
1
In Bezug auf transaction.atomic habe ich den Kontextmanager anstelle eines Dekorateurs verwendet. Ich sehe keinen Grund, bei jedem Modell eine atomare Transaktion zu verwenden, da dies nur wichtig ist, wenn das boolesche Feld wahr ist. Ich schlage vor, with transaction.atomic:innerhalb der if-Anweisung zusammen mit dem Speichern innerhalb der if-Anweisung zu verwenden. Fügen Sie dann einen else-Block hinzu und speichern Sie ihn im else-Block.
Alexbhandari
29

Anstatt das Bereinigen / Speichern von benutzerdefinierten Modellen zu verwenden, habe ich ein benutzerdefiniertes Feld erstellt, das die pre_saveMethode überschreibt django.db.models.BooleanField. Anstatt einen Fehler auszulösen, wenn ein anderes Feld vorhanden war True, habe ich alle anderen Felder erstellt, Falsewenn dies der Fall war True. Anstatt einen Fehler auszulösen, wenn das Feld war Falseund kein anderes Feld war True, habe ich das Feld als gespeichertTrue

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
quelle
2
Dies sieht viel sauberer aus als die anderen Methoden
Pistache
2
Ich mag diese Lösung auch, obwohl es möglicherweise gefährlich erscheint, wenn objects.update alle anderen Objekte auf False setzt, wenn die Modelle UniqueBoolean True sind. Wäre noch besser, wenn das UniqueBooleanField ein optionales Argument verwenden würde, um anzugeben, ob die anderen Objekte auf False gesetzt werden sollen oder ob ein Fehler ausgelöst werden soll (die andere sinnvolle Alternative). Angesichts Ihres Kommentars im elif, in dem Sie das Attribut auf true setzen möchten, sollten Sie auch Return Truezusetattr(model_instance, self.attname, True)
Andrew Chase
2
UniqueBooleanField ist nicht wirklich eindeutig, da Sie so viele falsche Werte haben können, wie Sie möchten. Nicht sicher, was ein besserer Name wäre ... OneTrueBooleanField? Was ich wirklich möchte, ist, dies in Kombination mit einem Fremdschlüssel erfassen zu können, so dass ich ein BooleanField haben kann, das nur einmal pro Beziehung True sein darf (z. B. hat eine CreditCard ein "primäres" Feld und eine FK für Benutzer und Die Kombination Benutzer / Primär ist einmal pro Verwendung True. In diesem Fall denke ich, dass Adams Antwort, die das Speichern außer Kraft setzt, für mich einfacher sein wird.
Andrew Chase
1
Es ist zu beachten, dass Sie mit dieser Methode in einem Zustand enden können, in dem keine Zeilen festgelegt sind, als würden trueSie die einzige trueZeile löschen .
rblk
11

Die folgende Lösung ist etwas hässlich, könnte aber funktionieren:

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)

Wenn Sie is_the_chosen_one auf False oder None setzen, ist es immer NULL. Sie können so viel NULL haben, wie Sie möchten, aber Sie können nur ein True haben.

semente
quelle
1
Die erste Lösung, an die ich auch gedacht habe. NULL ist immer eindeutig, sodass Sie immer eine Spalte mit mehr als einem NULL haben können.
Kaleissin
10

Beim Versuch, mit den Antworten hier über die Runden zu kommen, stelle ich fest, dass einige von ihnen dasselbe Problem erfolgreich angehen und jedes für unterschiedliche Situationen geeignet ist:

Ich würde wählen:

  • @semente : Respektiert die Einschränkung auf Datenbank-, Modell- und Administratorformularebene, während Django ORM so wenig wie möglich überschrieben wird. Darüber hinaus kann eswahrscheinlichin einer throughTabelle ManyToManyFieldin einer unique_togetherSituation verwendet werden.(Ich werde es überprüfen und berichten)

    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 : Trifft die Datenbank nur ein zusätzliches Mal und akzeptiert den aktuellen Eintrag als den ausgewählten. Sauber und elegant.

    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)  

Andere Lösungen, die für meinen Fall nicht geeignet, aber realisierbar sind:

@nemocorp überschreibt die cleanMethode zur Durchführung einer Validierung. Es wird jedoch nicht zurückgemeldet, welches Modell "das" ist, und dies ist nicht benutzerfreundlich. Trotzdem ist es ein sehr netter Ansatz, besonders wenn jemand nicht so aggressiv sein will wie @Flyte.

@ saul.shanabrook und @Thierry J. würden ein benutzerdefiniertes Feld erstellen, das entweder einen anderen "is_the_one" -Eintrag in a ändert Falseoder a auslöst ValidationError. Ich zögere nur, neue Funktionen für meine Django-Installation zu implementieren, es sei denn, dies ist unbedingt erforderlich.

@daigorocub : Verwendet Django-Signale. Ich finde es ein einzigartiger Ansatz und gebe einen Hinweis auf die Verwendung von Django-Signalen . Ich bin mir jedoch nicht sicher, ob dies streng genommen eine "ordnungsgemäße" Verwendung von Signalen ist, da ich dieses Verfahren nicht als Teil einer "entkoppelten Anwendung" betrachten kann.

raratiru
quelle
Danke für die Bewertung! Ich habe meine Antwort basierend auf einem der Kommentare ein wenig aktualisiert, falls Sie Ihren Code auch hier aktualisieren möchten.
Ellis Percival
@EllisPercival Danke für den Hinweis! Ich habe den Code entsprechend aktualisiert. Beachten Sie jedoch, dass models.Model.save () nichts zurückgibt .
Raratiru
Das ist gut. Es geht meistens nur darum, die erste Rückkehr auf einer eigenen Linie zu sparen. Ihre Version ist tatsächlich falsch, da sie die .save () nicht in der atomaren Transaktion enthält. Außerdem sollte es stattdessen 'with transaction.atomic ():' sein.
Ellis Percival
1
@EllisPercival OK, danke! In der Tat müssen wir alles zurücksetzen, falls die save()Operation fehlschlägt!
Raratiru
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

Sie können das obige Formular auch für Administratoren verwenden. Verwenden Sie einfach

class CharacterAdmin(admin.ModelAdmin):
    form = CharacterForm
admin.site.register(Character, CharacterAdmin)
Shadfc
quelle
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")

Dadurch wurde die Validierung im Basisverwaltungsformular verfügbar

Nemocorp
quelle
3

Es ist einfacher, diese Art von Einschränkung nach Django Version 2.2 zu Ihrem Modell hinzuzufügen. Sie können direkt verwenden UniqueConstraint.condition. Django Docs

Überschreiben Sie einfach Ihre Modelle class Metawie folgt:

class Meta:
    constraints = [
        UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one')
    ]
Mangofet
quelle
2

Und das ist alles.

def save(self, *args, **kwargs):
    if self.default_dp:
        DownloadPageOrder.objects.all().update(**{'default_dp': False})
    super(DownloadPageOrder, self).save(*args, **kwargs)
Palestamp
quelle
2

Mit einem ähnlichen Ansatz wie Saul, aber etwas anderem Zweck:

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

Diese Implementierung löst ein aus, ValidationErrorwenn versucht wird, einen anderen Datensatz mit dem Wert True zu speichern.

Außerdem habe ich das unique_forArgument hinzugefügt, das für jedes andere Feld im Modell festgelegt werden kann, um die Eindeutigkeit nur für Datensätze mit demselben Wert zu überprüfen, z.

class Phone(models.Model):
    user = models.ForeignKey(User)
    main = TrueUniqueBooleanField(unique_for='user', default=False)
Thierry J.
quelle
1

Bekomme ich Punkte für die Beantwortung meiner Frage?

Das Problem war, dass es sich in der Schleife befand, behoben durch:

    # 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
quelle
Nein, keine Punkte, um Ihre eigene Frage zu beantworten und diese Antwort zu akzeptieren. Es sind jedoch Punkte zu beachten, wenn jemand Ihre Antwort positiv bewertet. :)
dandan78
Sind Sie sicher, dass Sie nicht stattdessen Ihre eigene Frage hier beantworten wollten ? Grundsätzlich hatten Sie und @sampablokuper die gleiche Frage
j_syk
1

Ich habe einige dieser Lösungen ausprobiert und bin aus Gründen der Codekürzung zu einer anderen gekommen (ich muss keine Formulare überschreiben oder Methoden speichern). Damit dies funktioniert, kann das Feld in seiner Definition nicht eindeutig sein, aber das Signal stellt sicher, dass dies geschieht.

# 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)
Daigorocub
quelle
0

Update 2020, um Anfängern die Arbeit zu erleichtern:

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()

Wenn Sie möchten, dass der eindeutige Boolesche Wert False ist, tauschen Sie natürlich einfach jede Instanz von True gegen False aus und umgekehrt.

Jay
quelle