Unique BooleanField value in Django?

25,973

Solution 1

Whenever I've needed to accomplish this task, what I've done is override the save method for the model and have it check if any other model has the flag already set (and turn it off).

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)

Solution 2

I'd override the save method of the model and if you've set the boolean to True, make sure all others are set to 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)

I tried editing the similar answer by Adam, but it was rejected for changing too much of the original answer. This way is more succinct and efficient as the checking of other entries is done in a single query.

Solution 3

Instead of using custom model cleaning/saving, I created a custom field overriding the pre_save method on django.db.models.BooleanField. Instead of raising an error if another field was True, I made all other fields False if it was True. Also instead of raising an error if the field was False and no other field was True, I saved it the field as 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)

Solution 4

It is simpler to add this kind of constraint to your model after Django version 2.2. You can directly use UniqueConstraint.condition. Django Docs

Just override your models class Meta like this:

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

Solution 5

Trying to make ends meet with the answers here, I find that some of them address the same issue successfully and each one is suitable in different situations:

I would choose:

  • @semente: Respects the constraint at the database, model and admin form levels while it overrides Django ORM the least possible. Moreover it can be used inside a through table of a ManyToManyField in aunique_together situation.

      class MyModel(models.Model):
          is_the_chosen_one = models.BooleanField(null=True, 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)
    

    Update: NullBooleanField will be deprecated by Django-4.0, for BooleanField(null=True).

  • @Ellis Percival: Hits the database only one extra time and accepts the current entry as the chosen one. Clean and 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)  
    

Other solutions not suitable for my case but viable:

@nemocorp is overriding the clean method to perform a validation. However, it does not report back which model is "the one" and this is not user friendly. Despite that, it is a very nice approach especially if someone does not intend to be as aggressive as @Flyte.

@saul.shanabrook and @Thierry J. would create a custom field which would either change any other "is_the_one" entry to False or raise a ValidationError. I am just reluctant to impement new features to my Django installation unless it is absoletuly necessary.

@daigorocub: Uses Django signals. I find it a unique approach and gives a hint of how to use Django Signals. However I am not sure whether this is a -strictly speaking- "proper" use of signals since I cannot consider this procedure as part of a "decoupled application".

Share:
25,973

Related videos on Youtube

Admin
Author by

Admin

Updated on January 14, 2022

Comments

  • Admin
    Admin over 2 years

    Suppose my models.py is like so:

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

    I want only one of my Character instances to have is_the_chosen_one == True and all others to have is_the_chosen_one == False . How can I best ensure this uniqueness constraint is respected?

    Top marks to answers that take into account the importance of respecting the constraint at the database, model and (admin) form levels!

    • mathStudent001
      mathStudent001 over 14 years
      Good question. I'm also curious if its possible to set up such a constraint. I know that if you simply made it a unique constraint you'll end up with only two possible rows in your database ;-)
    • Matthew Schinckel
      Matthew Schinckel over 14 years
      Not necessarily: if you use a NullBooleanField, then you should be able to have: (a True, a False, any number of NULLs).
    • raratiru
      raratiru over 7 years
      According to my research, @semente answer, takes into account the importance of respecting the constraint at the database, model and (admin) form levels while it provides a great solution even for a through table of ManyToManyField that needs a unique_together constraint.
  • dandan78
    dandan78 almost 13 years
    No, no points for answering your own question and accepting that answer. However, there are points to be made if somebody upvotes your answer. :)
  • j_syk
    j_syk almost 13 years
    Are you sure you didn't mean to answer your own question here instead? Basically you and @sampablokuper had the same question
  • Marek
    Marek over 11 years
    I'd just change 'def save(self):' to: 'def save(self, *args, **kwargs):'
  • scytale
    scytale over 11 years
    I tried to edit this to change save(self) to save(self, *args, **kwargs) but the edit was rejected. Could any of the reviewers take time to explain why - since this would seem to be consistent with Django best practice.
  • pistache
    pistache over 11 years
    This looks far more clean than the other methods
  • kaleissin
    kaleissin almost 11 years
    The first solution I thought of also. NULL is always unique so you can always have a column with more than one NULL.
  • Andrew Chase
    Andrew Chase about 10 years
    I like this solution as well, although it seems potentially dangerous to have the objects.update set all other objects to False in the case where the models UniqueBoolean is True. Would be even better if the UniqueBooleanField took an optional argument to indicate whether the other objects should be set to False or if an error should be raised (the other sensible alternative). Also, given your comment in the elif, where you want to set the attribute to true, I think you should change Return True to setattr(model_instance, self.attname, True)
  • Andrew Chase
    Andrew Chase about 10 years
    UniqueBooleanField isn't really unique since you can have as many False values as you want. Not sure what a better name would be... OneTrueBooleanField? What I really want is to be able to scope this in combination with a foreign key so that I could have a BooleanField that was only allowed to be True once per relationship (e.g. a CreditCard has a "primary" field and a FK to User and the User/Primary combination is True once per use). For that case I think Adam's answer overriding save will be more straightforward for me.
  • Ellis Percival
    Ellis Percival almost 10 years
    I tried editing to remove the need for try/except and to make the process more efficient but it was rejected.. Instead of get()ing the Character object and then save()ing it again, you just need to filter and update, which produces just one SQL query and helps keep the DB consistent: if self.is_the_chosen_one: <newline> Character.objects.filter(is_the_chosen_one=True).update(is_t‌​he_chosen_one=False) <newline> super(Character, self).save(*args, **kwargs)
  • Mitar
    Mitar about 8 years
    I think this is the best answer, but I would suggest wrapping save into a @transaction.atomic transaction. Because it could happen that you remove all flags, but then saving fails and you end up with all characters not chosen.
  • Ellis Percival
    Ellis Percival about 8 years
    Thank you for saying so. You are absolutely right and I'll update the answer.
  • rblk
    rblk over 6 years
    It should be noted that this method allows you end up in a state with no rows set as true if you delete the only true row.
  • Pawel Furmaniak
    Pawel Furmaniak almost 6 years
    @Mitar @transaction.atomic also protects from race condition.
  • u.unver34
    u.unver34 over 5 years
    I cannot suggest any better method to accomplish that task but i want to say that, don't ever trust save or clean methods if you are running a web application which you might take a few of requests to an endpoint at very same moment. You still must implement a safer way maybe on database level.
  • Arturo
    Arturo about 5 years
    Best solution among all!
  • alexbhandari
    alexbhandari over 4 years
    Regarding transaction.atomic I used the context manager instead of a decorator. I see no reason to use atomic transaction on every model save as this only matters if the boolean field is true. I suggest using with transaction.atomic: inside the if statement along with saving inside the if. Then adding an else block and also saving in the else block.
  • alexbhandari
    alexbhandari over 4 years
    There is a better answer below. Ellis Percival's answer uses transaction.atomic which is important here. It is also more efficient using a single query.
  • Ellis Percival
    Ellis Percival over 4 years
    @alexbhandari nice spot! I've updated the answer. How does that look to you?
  • Ellis Percival
    Ellis Percival over 4 years
    Thanks for the review! I've updated my answer a little, based on one of the comments, in case you want to update your code here too.
  • raratiru
    raratiru over 4 years
    @EllisPercival Thank you for the hint! I updated the code accordingly. Bear in mind though that models.Model.save() does not return something.
  • Ellis Percival
    Ellis Percival over 4 years
    That's fine. It's mostly just to save having the first return on its own line. Your version is actually incorrect, as it doesn't include the .save() in the atomic transaction. Plus, it should be 'with transaction.atomic():' instead.
  • raratiru
    raratiru over 4 years
    @EllisPercival OK, thank you! Indeed, we need everything rolled back, should the save() operation fails!
  • Pawel Decowski
    Pawel Decowski over 4 years
    Don’t forget to provide a custom QuerySet with overridden update method to prevent from updating multiple objects’ is_the_chosen_one to True in one go.
  • Lane
    Lane about 3 years
    This is simple and concise. Great! Thanks.
  • S.D.
    S.D. almost 3 years
    This is the way to go.