Common patterns
django-queryable-properties offers some fully implemented properties for common code patterns out of the box. All of them are class-based and parametrizable for their specific use case (while still supporting all Common property arguments) and are supposed to help remove boilerplate for recurring types of properties while making them usable in querysets at the same time.
Specialized properties
The properties in this category are designed for very specific use cases and are not based on annotations.
ValueCheckProperty
: Checking a field for one or multiple specific values
Properties on model objects are often used to check if an attribute on a model instance contains a specific value (or one of multiple values). This is often done for fields with choices as it allows to implement the check for a certain choice value in one place instead of redefining it whenever the field should be checked for the value. However, the pattern is not limited to fields with choices.
Imagine that the ApplicationVersion
example model would also contain a field that contains information about the
type of release, e.g. if a certain version is an alpha, a beta, etc.
It would be well-advised to use a field with choices for this value and to also define properties to check for the
individual values to only define these checks once.
Without django-queryable-properties, the implementation could look similar to this:
from django.db import models
from django.utils.translation import ugettext_lazy as _
class ApplicationVersion(models.Model):
ALPHA = 'a'
BETA = 'b'
STABLE = 's'
RELEASE_TYPE_CHOICES = (
(ALPHA, _('Alpha')),
(BETA, _('Beta')),
(STABLE, _('Stable')),
)
... # other fields
release_type = models.CharField(max_length=1, choices=RELEASE_TYPE_CHOICES)
@property
def is_alpha(self):
return self.release_type == self.ALPHA
@property
def is_beta(self):
return self.release_type == self.BETA
@property
def is_stable(self):
return self.release_type == self.STABLE
@property
def is_unstable(self):
return self.release_type in (self.ALPHA, self.BETA)
Instead of defining the properties like this, the property class
queryable_properties.properties.ValueCheckProperty
could be used:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from queryable_properties.managers import QueryablePropertiesManager
from queryable_properties.properties import ValueCheckProperty
class ApplicationVersion(models.Model):
ALPHA = 'a'
BETA = 'b'
STABLE = 's'
RELEASE_TYPE_CHOICES = (
(ALPHA, _('Alpha')),
(BETA, _('Beta')),
(STABLE, _('Stable')),
)
... # other fields
release_type = models.CharField(max_length=1, choices=RELEASE_TYPE_CHOICES)
objects = QueryablePropertiesManager()
is_alpha = ValueCheckProperty('release_type', ALPHA)
is_beta = ValueCheckProperty('release_type', BETA)
is_stable = ValueCheckProperty('release_type', STABLE)
is_unstable = ValueCheckProperty('release_type', ALPHA, BETA)
Instances of this property class take the path of the attribute to check as their first parameter in addition to any
number of parameters that represent the values to check for - if one of them matches when the property is accessed on
a model instance, the property will return True
(otherwise False
).
Not only does this property class allow to achieve the same functionality with less code, but it offers even more
functionality due to being a queryable property.
The class implements both queryset filtering as well as annotating (based on Django’s Case
/When
objects), so
the properties can be used in querysets as well:
stable_versions = ApplicationVersion.objects.filter(is_stable=True)
non_alpha_versions = ApplicationVersion.objects.filter(is_alpha=False)
ApplicationVersion.objects.order_by('is_unstable')
For a quick overview, the ValueCheckProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes |
Annotation |
Yes (Django 1.8 or higher) |
Updating |
No |
Attribute paths
The attribute path specified as the first parameter can not only be a simple field name like in the example above,
but also a more complex path to an attribute using dot-notation - basically the same way as for Python’s
operator.attrgetter
.
For queryset operations, the dots are then simply replaced by the lookup separator (__
), so an attribute path
my.attr
becomes my__attr
in queries.
This is especially useful to reach fields of related model instances via foreign keys, but it also allows to be more
creative since the path simply needs to make sense both on the object-level as well as in queries.
For example, a DateField
may be defined as date_field = models.DateField()
, which would allow a
ValueCheckProperty
to be set up with the path date_field.year
.
This works because the date
object has an attribute year
on the object-level and Django offers a year
transform for querysets (so date_field__year
does in fact work).
However, this specific example requires at least Django 1.9 as older versions don’t allow to combine transforms and
lookups.
In general, this means that the attribute path does not have to refer to an actual field, which also means that it may
refer to another queryable property (which needs to support the in
lookup to be able to filter correctly).
Unlike Python’s operator.attrgetter
, the property will also automatically catch some exceptions during getter access
(if any of them occur, the property considers none of the configured values as matching):
AttributeError
s if an intermediate object isNone
(e.g. if a path isa.b
and thea
attribute already returnsNone
, then the attribute error when accessingb
will be caught). This is intended to make working with nullable fields easier. Any other kind ofAttributeError
will still be raised.Any
ObjectDoesNotExist
errors raised by Django, which are raised e.g. when accessing a reverse One-To-One relation with a missing value. This is intended to make working with these kinds of relations easier.
RangeCheckProperty
: Checking if a value is contained in a range defined by two fields
A common pattern that uses a property is having a model with two attributes that define a lower and an upper limit and
a property that checks if a certain value is contained in that range.
These fields may be numerical fields (IntegerField
, DecimalField
, etc.) or something like date fields
(DateField
, DateTimeField
, etc.) - basically anything that allows “greater than” and “lower than” comparisons.
As an example, the ApplicationVersion
example model could contain two such date fields to express the period in which
a certain app version is supported, which could look similar to this:
from django.db import models
from django.utils import timezone
class ApplicationVersion(models.Model):
... # other fields
supported_from = models.DateTimeField()
supported_until = models.DateTimeField()
@property
def is_supported(self):
return self.supported_from <= timezone.now() <= self.supported_until
Instead of defining the properties like this, the property class
queryable_properties.properties.RangeCheckProperty
could be used:
from django.db import models
from django.utils import timezone
from queryable_properties.managers import QueryablePropertiesManager
from queryable_properties.properties import RangeCheckProperty
class ApplicationVersion(models.Model):
... # other fields
supported_from = models.DateTimeField()
supported_until = models.DateTimeField()
objects = QueryablePropertiesManager()
is_supported = RangeCheckProperty('supported_from', 'supported_until', timezone.now)
Instances of this property class take the paths of the attributes for the lower and upper limits as their first and
second arguments.
Both values may also be more complex attribute paths in dot-notation - the same behavior as for the attribute path of
ValueCheckProperty
objects apply (refer to chapter Attribute paths above).
If one of the limiting values is None
or an exception is caught, the value is considered missing (see next sub-
chapter).
The third mandatory parameter for RangeCheckProperty
objects is the value to check against, which may either be a
static value or a callable that can be called without any argument and that returns the actual value (timezone.now
in the example above), similar to the default
option of Django’s model fields.
Not only does this property class allow to achieve the same functionality with less code, but it offers even more
functionality due to being a queryable property.
The class implements both queryset filtering as well as annotating (based on Django’s Case
/When
objects), so the
properties can be used in querysets as well:
currently_supported = ApplicationVersion.objects.filter(is_supported=True)
not_supported = ApplicationVersion.objects.filter(is_supported=False)
ApplicationVersion.objects.order_by('is_supported')
For a quick overview, the RangeCheckProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes |
Annotation |
Yes (Django 1.8 or higher) |
Updating |
No |
Range configuration
RangeCheckProperty
objects also allow further configuration to tweak the configured range via some optional
parameters:
include_boundaries
Determines if a value exactly equal to one of the limits is considered a part of the range (default:
True
).include_missing
Determines if a missing value for either boundary is considered part of the range (default:
False
).in_range
Determines if the property should return
True
if the value is contained in the configured range (this is the default) or if it should returnTrue
if the value is outside of the range.
It should be noted that the include_boundaries
and include_missing
parameters are applied first to define the
range (which values are considered inside the range between the two values) and the in_range
parameter is applied
afterwards to potentially invert the result (in the case of in_range=False
).
This means that setting include_missing=True
defines that missing values are part of the range and a value of
in_range=False
would then invert this range, meaning that missing values would not lead to a value of True
since they are configured to be in the range while the property is set up to return True
for values outside of the
range.
For a quick reference, all possible configuration combinations are listed in the following table:
|
|
|
returns |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note
The attribute paths passed to RangeCheckProperty
may also refer to other queryable properties as long as these
properties allow filtering with the lt
/lte
and gt
/gte
lookups (depending on the value of
include_boundaries
) and potentially the isnull
lookup (depending on the value of include_missing
).
MappingProperty
: Mapping field values to other values
The property class queryable_properties.properties.MappingProperty
streamlines a very simple pattern: mapping
the values of an attribute (most likely a field) to different values.
While there is nothing special about this on an object basis, it allows to introduce values into querysets that
otherwise are not database values.
The value mapping inside querysets is achieved using CASE
/WHEN
expressions based on Django’s Case
/When
objects, which means that this property class can only be properly used in Django versions that provide these features
(1.8+).
A common use case for this might be to set up a MappingProperty
that simply works with a choice field and uses the
choice definitions themselves as its mappings.
This allows to introduce the (most likely translatable) choice verbose names into the query, which in turn allows to
order the queryset by the translated verbose names, providing sensible ordering no matter what language an
application is used in.
For the release type values in an example above, this could look like this:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from queryable_properties.managers import QueryablePropertiesManager
from queryable_properties.properties import MappingProperty
class ApplicationVersion(models.Model):
ALPHA = 'a'
BETA = 'b'
STABLE = 's'
RELEASE_TYPE_CHOICES = (
(ALPHA, _('Alpha')),
(BETA, _('Beta')),
(STABLE, _('Stable')),
)
... # other fields
release_type = models.CharField(max_length=1, choices=RELEASE_TYPE_CHOICES)
objects = QueryablePropertiesManager()
release_type_verbose_name = MappingProperty('release_type', models.CharField(), RELEASE_TYPE_CHOICES)
In a view, one could then perform a query similar to the following to order the ApplicationVersion
objects by
their translated verbose name, which may lead to a different ordering depending on the user’s language:
ApplicationVersion.objects.order_by('release_type_verbose_name')
This is, however, not the only way MappingProperty
objects can be used - any attribute values may be translated
into any other values that can be represented in database queries and then used in querysets.
MappingProperty
objects may be initialized with up to four parameters:
attribute_path
(required)An attribute path to the attribute whose values are to be mapped to other values - the same behavior as for the attribute path of
ValueCheckProperty
objects apply (refer to chapter Attribute paths above).output_field
(required)A field instance that is used to represent the translated values in queries.
mappings
(required)Defines the actual mappings as an iterable of 2-tuples, where the first value is the expected attribute value and the second value is the translated value. This can be almost any type of iterable - it just needs to be able to be iterated multiple times as the whole iterable is evaluated any time the property is accessed on an object or in queries (generators are therefore not usable).
default
(optional)Defines a default value, which defaults to
None
. Whenever an attribute value is encountered that has no mapping via the third parameter, this default value is returned instead.
Note
Whenever the mapping output values are actually accessed (by accessing the property on an object or by referencing it in a queryset), lazy values (like the translations in the example above) are evaluated. Property access or queryset references should therefore be performed as late as possible when dealing with lazy mapping values. For queryset operations, the translated values are also automatically wrapped in Value objects.
Note
The attribute path passed to MappingProperty
may also refer to another queryable property as long as this
property allows filtering with the exact
lookup.
For a quick overview, the MappingProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes (Django 1.8 or higher) |
Annotation |
Yes (Django 1.8 or higher) |
Updating |
No |
Annotation-based properties
The properties in this category are all Annotation-based properties, which means their getter
implementation will also perform a database query.
All of the listed properties therefore also take an additional cached
argument in their initializer that allows
to mark individual properties as having a Cached getter.
This can improve performance since the query will only be executed on the first getter access at the cost of
potentially not working with an up-to-date value.
AnnotationProperty
: Static annotations
The property class queryable_properties.properties.AnnotationProperty
represents the most simple common
annotation-based property.
It can be instanciated using any annotation and will use that annotation both in queries as well as to provide its
getter value.
This, however, means that the AnnotationProperty
is only intended to be used with static/fixed annotations without
any dynamic components as its objects are set up by passing the annotation to the initializer.
As an example, the version_str
property from the annotation Implementation section could be
reduced to (not recommended):
from django.db.models import Model, Value
from django.db.models.functions import Concat
from queryable_properties.properties import AnnotationProperty
class ApplicationVersion(Model):
... # other fields/properties
version_str = AnnotationProperty(Concat('major', Value('.'), 'minor'))
Note
This example is only supposed to demonstrate how to set up an AnnotationProperty
.
Implementing a Concat
annotation like this is not recommended as even the getter will perform a query, even
though concatenating field values on the object level could simply be done without involving the database.
For a quick overview, the AnnotationProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes |
Annotation |
Yes |
Updating |
No |
AggregateProperty
: Simple aggregates
django-queryable-properties also comes with a property class for simple aggregates that simply takes an aggregate
object and uses it for both queryset annotations as well as the getter.
This is therefore not entirely different from the AnnotationProperty
class shown above.
The main difference between the two is that while AnnotationProperty
uses QuerySet.annotate
to query the getter
value, AggregateProperty
uses QuerySet.aggregate
, which is slightly more efficient.
Using AggregateProperty
for aggregate annotations might also make code more clear/readable.
As an example, the Application
model could receive a simple property that returns the number of versions like the
one in the Implementation section of annotation-based properties.
queryable_properties.properties.AggregateProperty
allows to implement this in an even more condensed form:
from django.db.models import Count, Model
from queryable_properties.properties import AggregateProperty
class Application(Model):
... # other fields/properties
version_count = AggregateProperty(Count('versions'))
Note
Since this property deals with aggregates, the notes Regarding aggregate annotations across relations apply when using such properties across relations in querysets.
For a quick overview, the AggregateProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes |
Annotation |
Yes |
Updating |
No |
Subquery-based properties (Django 1.11 or higher)
The properties in this category are all based on custom subqueries, i.e. they utilize Django’s Subquery
objects.
They are therefore Annotation-based properties, which means their getter implementation will
also perform a database query.
Due to the utilization of Subquery
objects, these properties can only be used in conjunction with a Django version
that supports custom subqueries, i.e. Django 1.11 or higher.
All subquery-based properties take a queryset that will be used to generate the custom subquery as their first
argument.
This queryset is always expected to be a regular queryset, i.e. not a Subquery
object - the properties will
build these objects on their own using the given queryset.
The specified queryset can (and in most cases should) contain OuterRef
objects to filter the subquery’s rows based
on the outer query.
These OuterRef
objects will always be based on the model the property is defined on - all fields of that model or
related fields starting from that model can therefore be referenced.
Instead of specifying a queryset directly, the subquery-based properties can also take a callable without any arguments as their first parameter, which in turn must return the queryset. This is useful in cases where the model class for the subquery’s queryset cannot be imported on the module level or is defined later in the same module.
SubqueryFieldProperty
: Getting a field value from a subquery
The property class queryable_properties.properties.SubqueryFieldProperty
allows to retrieve the value of any
field from the specified subquery.
The field does not have to be a static model field, but may also be an annotated field (which can even be used to work
around the problem described in Regarding aggregate annotations across relations) or even a
queryable property as long as it was selected as described in Selecting annotations.
Based on the version_str
property for the ApplicationVersion
shown in the Implementation
documentation for annotatable properties, an example property could be implemented for the Application
model that
determines the highest version for each application via a subquery:
from django.db import models
from queryable_properties.properties import SubqueryFieldProperty
class Application(models.Model):
... # other fields/properties
highest_version = SubqueryFieldProperty(
(ApplicationVersion.objects.select_properties('version_str')
.filter(application=models.OuterRef('pk'))
.order_by('-major', '-minor')),
field_name='version_str', # The field to extract the property value from
output_field=models.CharField() # Only required in cases where Django can't determine the type on its own
)
Note
Since the property can only return a single value per object, the subquery is limited to the first row (the
specified queryset and field name is essentially transformed into Subquery(queryset.values(field_name)[:1])
).
If a subquery returns multiple rows, it should therefore be ordered in a way that puts the desired value into the
first row.
For a quick overview, the SubqueryFieldProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes |
Annotation |
Yes |
Updating |
No |
SubqueryExistenceCheckProperty
: Checking whether or not certain objects exist via a subquery
The property class queryable_properties.properties.SubqueryExistenceCheckProperty
is similar to the
queryable_properties.properties.RelatedExistenceCheckProperty
mentioned above, but can be used to perform
any kind of existence check via a subquery.
The objects whose existence is to be determined does therefore not have to be related to the class the property is
defined on via a ForeignKey
or another relation field.
To perform this check, the given queryset is wrapped into an Exists
object, which may also be negated using the
property’s negated
argument.
For an example use case, certain applications may be so popular that they receive their own category with the same
name as the application.
To determine whether or not an application has its own category, a SubqueryExistenceCheckProperty
could be used:
from django.db import models
from queryable_properties.properties import SubqueryExistenceCheckProperty
class Application(models.Model):
... # other fields/properties
has_own_category = SubqueryExistenceCheckProperty(Category.objects.filter(name=models.OuterRef('name')))
For a quick overview, the SubqueryExistenceCheckProperty
offers the following queryable property features:
Feature |
Supported |
---|---|
Getter |
Yes |
Setter |
No |
Filtering |
Yes |
Annotation |
Yes |
Updating |
No |