It depends whether your property
is a means-to-an-end or an end in itself.
If you want this kind of "override" (or "fallback") behavior when filtering querysets (without first having to evaluate them), I don't think properties can do the trick. As far as I know, Python properties do not work at the database level, so they cannot be used in queryset filters. Note that you can use _foo
in the filter (instead of foo
), as it represents an actual table column, but then the override logic from your get_foo()
won't apply.
However, if your use-case allows it, the Coalesce()
class from django.db.models.functions
(docs) might help.
Coalesce()
... Accepts a list of at least two field names or
expressions and returns the first non-null value (note that an empty
string is not considered a null value). ...
This implies that you can specify bar
as an override for foo
using Coalesce('bar','foo')
. This returns bar
, unless bar
is null
, in which case it returns foo
. Same as your get_foo()
(except it doesn't work for empty strings), but on the database level.
The question that remains is how to implement this.
If you don't use it in a lot of places, simply annotating the queryset may be easiest. Using your example, without the property stuff:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
Then make your query like this:
from django.db.models.functions import Coalesce
queryset = MyModel.objects.annotate(bar_otherwise_foo=Coalesce('bar', 'foo'))
Now the items in your queryset have the magic attribute bar_otherwise_foo
, which can be filtered on, e.g. queryset.filter(bar_otherwise_foo='what I want')
, or it can be used directly on an instance, e.g. print(queryset.all()[0].bar_otherwise_foo)
The resulting SQL query from queryset.query
shows that Coalesce()
indeed works at the database level:
SELECT "myapp_mymodel"."id", "myapp_mymodel"."foo", "myapp_mymodel"."bar",
COALESCE("myapp_mymodel"."bar", "myapp_mymodel"."foo") AS "bar_otherwise_foo"
FROM "myapp_mymodel"
Note: you could also call your model field _foo
then foo=Coalesce('bar', '_foo')
, etc. It would be tempting to use foo=Coalesce('bar', 'foo')
, but that raises a ValueError: The annotation 'foo' conflicts with a field on the model.
There must be several ways to create a DRY implementation, for example writing a custom lookup, or a custom(ized) Manager.
A custom manager is easily implemented as follows (see example in docs):
class MyModelManager(models.Manager):
""" standard manager with customized initial queryset """
def get_queryset(self):
return super(MyModelManager, self).get_queryset().annotate(
bar_otherwise_foo=Coalesce('bar', 'foo'))
class MyModel(models.Model):
objects = MyModelManager()
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
Now every queryset for MyModel
will automatically have the bar_otherwise_foo
annotation, which can be used as described above.
Note, however, that e.g. updating bar
on an instance will not update the annotation, because that was made on the queryset. The queryset will need to be re-evaluated first, e.g. by getting the updated instance from the queryset.
Perhaps a combination of a custom manager with annotation and a Python property
could be used to get the best of both worlds (example at CodeReview).