Valore BooleanField unico in Django?


90

Supponiamo che il mio models.py sia così:

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

Voglio che solo una delle mie Characteristanze abbia is_the_chosen_one == Truee tutte le altre is_the_chosen_one == False. Come posso garantire al meglio il rispetto di questo vincolo di unicità?

Il massimo dei voti alle risposte che tengono conto dell'importanza di rispettare il vincolo a livello di database, modello e modulo (admin)!


4
Buona domanda. Sono anche curioso di sapere se è possibile impostare un tale vincolo. So che se lo rendi semplicemente un vincolo unico, finirai con solo due possibili righe nel tuo database ;-)
Andre Miller,

Non necessariamente: se usi un NullBooleanField, dovresti essere in grado di avere: (un True, un False, qualsiasi numero di NULL).
Matthew Schinckel

Secondo la mia ricerca , @semente risposta, tiene conto dell'importanza di rispettare il vincolo a livello di database, modello e modulo (admin) mentre fornisce un'ottima soluzione anche per una throughtabella ManyToManyFieldche necessita di un unique_togethervincolo.
raratiru

Risposte:


66

Ogni volta che ho bisogno di eseguire questa operazione, ciò che ho fatto è sovrascrivere il metodo di salvataggio per il modello e fargli controllare se qualsiasi altro modello ha il flag già impostato (e disattivarlo).

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)

3
Vorrei solo cambiare 'def save (self):' in: 'def save (self, * args, ** kwargs):'
Marek

8
Ho provato a modificarlo per passare save(self)a save(self, *args, **kwargs)ma la modifica è stata rifiutata. Qualcuno dei revisori potrebbe richiedere tempo per spiegare perché, dal momento che questo sembrerebbe coerente con le migliori pratiche di Django.
scytale

14
Ho provato a modificare per rimuovere la necessità di provare / tranne e per rendere il processo più efficiente, ma è stato rifiutato .. Invece di get()inserire l'oggetto Character e quindi save()reinserirlo, devi solo filtrare e aggiornare, il che produce solo una query SQL e aiuta a mantenere il database coerente: 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
Non posso suggerire alcun metodo migliore per eseguire tale attività, ma voglio dire che, non fidarti mai dei metodi di salvataggio o pulizia se stai eseguendo un'applicazione web che potresti portare alcune richieste a un endpoint nello stesso momento. È comunque necessario implementare un modo più sicuro, magari a livello di database.
u.unver34

1
C'è una risposta migliore di seguito. La risposta di Ellis Percival usa ciò transaction.atomicche è importante qui. È anche più efficiente utilizzando una singola query.
alexbhandari

36

Sostituirei il metodo di salvataggio del modello e se hai impostato il valore booleano su True, assicurati che tutti gli altri siano impostati su 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)

Ho provato a modificare la risposta simile di Adam, ma è stata rifiutata per aver modificato troppo la risposta originale. In questo modo è più succinto ed efficiente in quanto il controllo di altre voci viene eseguito in una singola query.


8
Penso che questa sia la risposta migliore, ma suggerirei di saveconcludere una @transaction.atomictransazione. Perché può succedere che rimuovi tutti i flag, ma poi il salvataggio fallisce e ti ritrovi con tutti i personaggi non scelti.
Mitar

Grazie per averlo detto. Hai perfettamente ragione e aggiornerò la risposta.
Ellis Percival

@Mitar @transaction.atomicprotegge anche dalle condizioni di gara.
Pawel Furmaniak

2
La migliore soluzione tra tutte!
Arturo

1
Per quanto riguarda transaction.atomic ho usato il context manager invece di un decoratore. Non vedo alcun motivo per utilizzare la transazione atomica su ogni modello, tranne perché è importante solo se il campo booleano è vero. Suggerisco di utilizzare with transaction.atomic:l'istruzione if insieme al salvataggio all'interno di if. Quindi aggiungendo un altro blocco e salvando anche nel blocco else.
alexbhandari

29

Invece di utilizzare la pulizia / salvataggio del modello personalizzato, ho creato un campo personalizzato che sovrascrive il pre_savemetodo django.db.models.BooleanField. Invece di generare un errore se c'era un altro campo True, ho creato tutti gli altri campi Falsese lo era True. Inoltre, invece di generare un errore se il campo era Falsee nessun altro campo lo era True, l'ho salvato comeTrue

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)

2
Questo sembra molto più pulito rispetto agli altri metodi
pistache il

2
Mi piace anche questa soluzione, anche se sembra potenzialmente pericoloso avere object.update che imposti tutti gli altri oggetti su False nel caso in cui i modelli UniqueBoolean siano True. Sarebbe ancora meglio se UniqueBooleanField prendesse un argomento opzionale per indicare se gli altri oggetti devono essere impostati su False o se deve essere generato un errore (l'altra alternativa sensata). Inoltre, dato il tuo commento nell'elif, dove vuoi impostare l'attributo su true, penso che dovresti cambiare Return Trueinsetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanField non è davvero unico poiché puoi avere tutti i valori False che desideri. Non sei sicuro di quale sarebbe un nome migliore ... OneTrueBooleanField? Quello che voglio veramente è essere in grado di definire l'ambito di questo in combinazione con una chiave esterna in modo da poter avere un BooleanField che poteva essere True solo una volta per relazione (ad esempio una CreditCard ha un campo "primario" e un FK per Utente e la combinazione utente / principale è vera una volta per utilizzo). In tal caso, penso che la risposta di Adam, che esclude il salvataggio, sarà più semplice per me.
Andrew Chase

1
Va notato che questo metodo consente di finire in uno stato senza righe impostate come truese si eliminasse l'unica trueriga.
rblk

11

La seguente soluzione è un po 'brutta ma potrebbe funzionare:

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 imposti is_the_chosen_one su False o None, sarà sempre NULL. Puoi avere NULL quanto vuoi, ma puoi averne solo uno True.


1
La prima soluzione a cui ho pensato anche. NULL è sempre univoco, quindi puoi sempre avere una colonna con più di un NULL.
kaleissin

10

Cercando di sbarcare il lunario con le risposte qui, trovo che alcune di esse affrontino lo stesso problema con successo e ognuna sia adatta a situazioni diverse:

Sceglierei:

  • @semente : rispetta il vincolo a livello di database, modello e modulo di amministrazione mentre sovrascrive il meno possibile Django ORM. Inoltre puòprobabilmenteessere utilizzato all'interno di una throughtabella di ManyToManyFielduna unique_togethersituazione.(Lo controllerò e riferirò)

    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 : colpisce il database solo una volta in più e accetta la voce corrente come quella prescelta. Pulito ed 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)  
    

Altre soluzioni non adatte al mio caso ma praticabili:

@nemocorp sta sovrascrivendo il cleanmetodo per eseguire una convalida. Tuttavia, non riporta quale modello è "quello" e questo non è facile da usare. Nonostante ciò, è un approccio molto carino, specialmente se qualcuno non intende essere aggressivo come @Flyte.

@ saul.shanabrook e @Thierry J. creerebbero un campo personalizzato che cambierebbe qualsiasi altra voce "is_the_one" Falseo solleverebbe un ValidationError. Sono solo riluttante a migliorare nuove funzionalità per la mia installazione Django a meno che non sia assolutamente necessario.

@daigorocub : utilizza i segnali Django. Lo trovo un approccio unico e dà un suggerimento su come utilizzare i segnali Django . Tuttavia non sono sicuro che si tratti di un uso "corretto" dei segnali poiché non posso considerare questa procedura come parte di una "applicazione disaccoppiata".


Grazie per la recensione! Ho aggiornato un po 'la mia risposta, sulla base di uno dei commenti, nel caso in cui desideri aggiornare il tuo codice anche qui.
Ellis Percival

@EllisPercival Grazie per il suggerimento! Ho aggiornato il codice di conseguenza. Tieni presente però che models.Model.save () non restituisce qualcosa.
raratiru

Va bene. È principalmente solo per risparmiare il primo ritorno sulla propria linea. La tua versione è effettivamente errata, poiché non include .save () nella transazione atomica. Inoltre, dovrebbe essere "with transaction.atomic ():" invece.
Ellis Percival

1
@EllisPercival OK, grazie! In effetti, è necessario ripristinare tutto, se l' save()operazione non riesce!
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

Puoi utilizzare il modulo sopra anche per l'amministratore, basta usare

class CharacterAdmin(admin.ModelAdmin):
    form = CharacterForm
admin.site.register(Character, CharacterAdmin)

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

In questo modo la convalida è disponibile nel modulo di amministrazione di base


4

È più semplice aggiungere questo tipo di vincolo al tuo modello dopo la versione 2.2 di Django. Puoi usare direttamente UniqueConstraint.condition. Django Docs

Sovrascrivi i tuoi modelli in class Metaquesto modo:

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

2

E questo è tutto.

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

2

Utilizzando un approccio simile a Saul, ma con uno scopo leggermente diverso:

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

Questa implementazione genererà un ValidationErrorquando si tenta di salvare un altro record con un valore di True.

Inoltre, ho aggiunto l' unique_forargomento che può essere impostato su qualsiasi altro campo nel modello, per verificare la vera unicità solo per i record con lo stesso valore, come ad esempio:

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

1

Ottengo punti rispondendo alla mia domanda?

il problema era che si trovava nel ciclo, risolto da:

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

No, nessun punto per rispondere alla tua domanda e accettare quella risposta. Tuttavia, ci sono punti da sottolineare se qualcuno vota positivamente la tua risposta. :)
dandan78

Sei sicuro di non voler rispondere alla tua stessa domanda qui ? Fondamentalmente tu e @sampablokuper avete avuto la stessa domanda
j_syk

1

Ho provato alcune di queste soluzioni e ne ho trovata un'altra, solo per motivi di brevità del codice (non è necessario sovrascrivere i moduli o salvare il metodo). Perché funzioni, il campo non può essere unico nella sua definizione, ma il segnale fa in modo che ciò accada.

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

0

Aggiornamento 2020 per rendere le cose meno complicate per i principianti:

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

Ovviamente, se vuoi che il valore booleano univoco sia False, devi semplicemente scambiare ogni istanza di True con False e viceversa.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.