Basics

Implementing queryable properties

There are two ways to implement a queryable property:

  • Using decorated methods directly on the model class (just like regular properties)

  • Implementing the queryable property as a class and using its instances as class attributes on the model class (much like model fields)

Say we’d want to implement a queryable property for the ApplicationVersion example model that simply returns the combined version information as a string. The two following sections show how to implement such a queryable property - for the sake of simplicity, the examples only show how to implement a getter and setter (which could also be implemented using a regular property). The following chapters of this documentation will show all available decorators, mixins and implementable methods in detail.

Decorator-based approach

The decorator-based approach uses the class queryable_properties.properties.queryable_property and its methods as decorators:

from django.db import models
from queryable_properties.properties import queryable_property


class ApplicationVersion(models.Model):
    ...

    @queryable_property
    def version_str(self):
        return '{major}.{minor}'.format(major=self.major, minor=self.minor)

    @version_str.setter
    def version_str(self, value):
        # Don't implement any validation to keep the example simple.
        self.major, self.minor = value.split('.')

Using the decorator methods without actually decorating

Python’s regular properties also allow to define properties without using property as a decorator. To do this, the individual methods that should make up the property can be passed to the property constructor:

class MyClass(object):

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    x = property(get_x, set_x)

Queryable properties do not allow to do this in the same way because of two reasons:

  • To encourage implementing properties using decorators, which is cleaner and makes code more readable.

  • Since queryable properties have a lot more functionality and options than regular properties, they would need to support a huge number of constructor parameters, which would make the constructor too complex and harder to maintain.

However, there are use cases where an option similar to the non-decorator usage of regular properties would be nice to have, e.g. when implementing a property without a getter or when the individual getter/setter methods are already present and cannot be easily deprecated in favor of the property. This is why queryable properties do support this form of defining a property - but in a slightly different way: the decorator methods can simply be chained together (this also works for all decorators introduced in later chapters).

from django.db import models
from queryable_properties.properties import queryable_property


class ApplicationVersion(models.Model):
    ...

    def get_version_str(self):
        return '{major}.{minor}'.format(major=self.major, minor=self.minor)

    def set_version_str(self, value):
        # Don't implement any validation to keep the example simple.
        self.major, self.minor = value.split('.')

    version_str = queryable_property(get_version_str).setter(set_version_str)

By not passing a getter function to the queryable_property constructor, a queryable property without a getter can be defined (queryable_property().setter(set_version_str) for the example above). This can even be used to make a getter-less queryable property while still decorating the setter (or mixing and matching chaining and decorating in general):

from django.db import models
from queryable_properties.properties import queryable_property


class ApplicationVersion(models.Model):
    ...

    version_str = queryable_property()  # Property without a getter

    @version_str.setter
    def version_str(self, value):
        # Don't implement any validation to keep the example simple.
        self.major, self.minor = value.split('.')

Class-based approach

Using the class-based approach, the queryable property is implemented as a subclass of queryable_properties.properties.QueryableProperty:

from django.db import models
from queryable_properties.properties import QueryableProperty, SetterMixin


class VersionStringProperty(SetterMixin, QueryableProperty):

    def get_value(self, obj):
        return '{major}.{minor}'.format(major=obj.major, minor=obj.minor)

    def set_value(self, obj, value):
        # Don't implement any validation to keep the example simple.
        obj.major, obj.minor = value.split('.')


class ApplicationVersion(models.Model):
    ...

    version_str = VersionStringProperty()

Common property arguments

Queryable properties that are created using either approach take additional, common keyword arguments that can be used to configure property instances further. These are:

verbose_name

A human-readable name for the property instance, similar to the verbose name of an instance of one of Django’s model fields. Used for UI representations of queryable properties. If no verbose name is set up for a property, one will be generated based on the property’s name.

For both the class-based and the decorator-based approach, these keyword arguments can be set via their respective constructor. For the example property above, this could look like the following example:

from django.utils.translation import gettext_lazy as _


class ApplicationVersion(models.Model):
    ...

    # Class-based
    version_str = VersionStringProperty(verbose_name=_('Full Version Number'))

    # Decorator-based
    @queryable_property(verbose_name=_('Full Version Number'))
    def version_str(self):
        ...

When to use which approach

It all depends on your needs and preferences, but a general rule of thumb is using the class-based approach to implement re-usable queryable properties or to be able to use inheritance. It would also be pretty easy to write parameterizable property classes by adding parameters to their __init__ methods.

Class-based implementations come, however, with the small disadvantage of having to define the property’s logic outside of the actual model class (unlike regular property implementations). It would therefore probably be preferable to use the decorator-based approach for unique, non-reusable implementations.

Enabling queryset operations

To actually interact with queryable properties in queryset operations, the queryset extensions provided by django-queryable-properties must be used since regular querysets cannot deal with queryable properties on their own.

The following sections describe how to properly set this up to either use the extensions by either applying them to querysets of models in general via managers or by creating querysets with the queryable properties extensions on demand.

Defining managers on models

The most common way to use the queryset extensions is by defining a manager that produces querysets with queryable properties functionality. The easiest way to do this is by simply using the queryable_properties.managers.QueryablePropertiesManager:

from queryable_properties.managers import QueryablePropertiesManager


class ApplicationVersion(models.Model):
    ...

    objects = QueryablePropertiesManager()

This manager allows to use the queryable properties in querysets created by this manager (e.g. via ApplicationVersion.objects.all()).

For scenarios where querysets or managers need other extensions or base classes, django-queryable-properties also offers a queryset class as well as mixins for managers or querysets that can be combined with other base classes:

When implementing custom queryset classes, a manager class can be generated from the queryset class using CustomQuerySet.as_manager() or CustomManager.from_queryset(CustomQuerySet).

Warning

Since queryable property interaction in querysets is tied to the specific extensions, those extensions are also required when trying to access queryable properties on related models. This means that using the manager approach, all models from which queries that interact with queryable properties are performed need to use a manager as described above, even if a model doesn’t implement its own queryable properties.

For example, if queryset filtering was implemented for the version_str property shown above, it could also be used in querysets of the Application model like this:

Application.objects.filter(versions__version_str='1.2')

To make this work, the objects manager of the Application model must also be a QueryablePropertiesManager, even if the model does not define queryable properties of its own.

If using a special manager just to access queryable properties on related models is not desirable, then the following approaches to apply the queryable properties extensions on demand should offer an alternative.

Creating managers/querysets on demand

The non-mixin classes provided by django-queryable-properties also allow to create managers or querysets on demand, regardless of the presence of a manager with queryable properties extensions on the corresponding model. Both the queryable_properties.managers.QueryablePropertiesManager and the queryable_properties.managers.QueryablePropertiesQuerySet offer a get_for_model method for this purpose:

from queryable_properties.managers import QueryablePropertiesManager, QueryablePropertiesQuerySet

# Create an ad hoc manager that produces querysets with queryable property extensions for the given model.
ad_hoc_manager = QueryablePropertiesManager.get_for_model(MyModel)
# Create an ad hoc queryset with queryable property extensions for the given model.
ad_hoc_queryset = QueryablePropertiesQuerySet.get_for_model(MyModel)

Note

Querysets created using QueryablePropertiesQuerySet.get_for_model use the model’s default manager to create the underlying queryset, i.e. the queryset is generated using model._default_manager.all() before the queryable properties extensions are applied.

Applying the extensions to existing managers/querysets on demand

There might be scenarios where interacting with queryable properties is desired in an existing queryset or manager. The mixin classes provided by django-queryable-properties allow to inject the queryable properties extensions into an existing queryset or manager using their apply_to method. Both the queryable_properties.managers.QueryablePropertiesManagerMixin and the queryable_properties.managers.QueryablePropertiesQuerySetMixin create a copy of the original object in the process, leaving said object untouched.

from queryable_properties.managers import QueryablePropertiesManagerMixin, QueryablePropertiesQuerySetMixin

# Create an ad hoc manager based off the given manager instance that produces querysets with queryable property
# extensions for the given model.
ad_hoc_manager = QueryablePropertiesManagerMixin.apply_to(some_manager)
# Create an ad hoc queryset with queryable property extensions for the given model.
some_queryset = MyModel.objects.filter(...).order_by(...)  # A queryset without queryable properties features.
ad_hoc_queryset = QueryablePropertiesQuerySetMixin.apply_to(some_queryset)
ad_hoc_queryset.select_properties(...)  # Now queryable properties features can be used.