Unique BooleanField value in Django?
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 aManyToManyField
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, forBooleanField(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".
Related videos on Youtube
Admin
Updated on January 14, 2022Comments
-
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 haveis_the_chosen_one == True
and all others to haveis_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 over 14 yearsGood 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 over 14 yearsNot necessarily: if you use a NullBooleanField, then you should be able to have: (a True, a False, any number of NULLs).
-
raratiru over 7 yearsAccording 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 ofManyToManyField
that needs aunique_together
constraint.
-
-
dandan78 almost 13 yearsNo, 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 almost 13 yearsAre you sure you didn't mean to answer your own question here instead? Basically you and @sampablokuper had the same question
-
Marek over 11 yearsI'd just change 'def save(self):' to: 'def save(self, *args, **kwargs):'
-
scytale over 11 yearsI tried to edit this to change
save(self)
tosave(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 over 11 yearsThis looks far more clean than the other methods
-
kaleissin almost 11 yearsThe first solution I thought of also. NULL is always unique so you can always have a column with more than one NULL.
-
Andrew Chase about 10 yearsI 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
tosetattr(model_instance, self.attname, True)
-
Andrew Chase about 10 yearsUniqueBooleanField 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 almost 10 yearsI 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 thensave()
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_the_chosen_one=False)
<newline>super(Character, self).save(*args, **kwargs)
-
Mitar about 8 yearsI 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 about 8 yearsThank you for saying so. You are absolutely right and I'll update the answer.
-
rblk over 6 yearsIt should be noted that this method allows you end up in a state with no rows set as
true
if you delete the onlytrue
row. -
Pawel Furmaniak almost 6 years@Mitar
@transaction.atomic
also protects from race condition. -
u.unver34 over 5 yearsI 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 about 5 yearsBest solution among all!
-
alexbhandari over 4 yearsRegarding 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 over 4 yearsThere 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 over 4 years@alexbhandari nice spot! I've updated the answer. How does that look to you?
-
Ellis Percival over 4 yearsThanks 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 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 over 4 yearsThat'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 over 4 years@EllisPercival OK, thank you! Indeed, we need everything rolled back, should the
save()
operation fails! -
Pawel Decowski over 4 yearsDon’t forget to provide a custom
QuerySet
with overriddenupdate
method to prevent from updating multiple objects’is_the_chosen_one
toTrue
in one go. -
Lane about 3 yearsThis is simple and concise. Great! Thanks.
-
S.D. almost 3 yearsThis is the way to go.