django-flexquery¶
Q
¶
The Q
implementation provided by this library contains a simple addition to that
of Django.
Creating a Q
object works as usual:
>>> from django_flexquery import Q
>>> q = Q(size__lt=10)
>>> q
<Q: (AND: ('size__lt', 10))>
But this implementation adds a prefix()
method, which allows prefixing some
related field’s name to the lookup keys of an existing Q
object. Since Q
objects can be nested, this is done recursively.
An example:
>>> q.prefix("fruits")
<Q: (AND: ('fruits__size__lt', 10))>
Nothing more to it. The real power comes when using these Q
objects with
FlexQuery
.
API¶
Extended Q implementation with support for prefixing lookup keys.
-
class
django_flexquery.q.
Q
(*args, _connector=None, _negated=False, **kwargs)¶ A custom Q implementation that allows prefixing existing Q objects with some related field name dynamically.
-
prefix
(prefix)¶ Recursively copies the Q object, prefixing all lookup keys.
The prefix and the existing filter key are delimited by the lookup separator __. Use this feature to delegate existing query constraints to a related field.
- Parameters
prefix (str) – Name of the related field to prepend to existing lookup keys. This isn’t restricted to a single relation level, something like “tree__fruit” is perfectly valid as well.
- Returns Q
-
FlexQuery
¶
The FlexQuery
class provides a decorator for functions declared on a custom
Manager
or QuerySet
:
from django_flexquery import FlexQuery, Manager, Q, QuerySet
# It's crucial to inherit from the QuerySet class of django_flexquery, because
# the FlexQuery's wouldn't make it over to a Manager derived using as_manager()
# with the stock Django implementation. That's the only difference however.
class FruitQuerySet(QuerySet):
# Declare a function that generates a Q object.
# base is a copy of the base QuerySet instance. It's not needed most of
# the time unless you want to embed a sub-query based on the current QuerySet
# into the Q object.
@FlexQuery.from_func
def large(base):
return Q(size__gte=10)
FruitQuerySet.large
now is a sub-type of FlexQuery
encapsulating the custom
function.
You can then derive a Manager
from FruitQuerySet
in two ways, using the
known Django API:
# Either use from_queryset() of the Manager class provided with this module.
class FruitManager(Manager.from_queryset(FruitQuerySet)):
...
# Or, if you don't want to add additional manager-only methods, create a Manager
# instance inside your model definition straight away.
class Fruit(Model):
objects = FruitQuerySet.as_manager()
...
When we assume such a Manager
being the default manager of a Fruit
model
with a size
field, we can now perform the following queries:
Fruit.objects.large()
Fruit.objects.filter(Fruit.objects.large.as_q())
Internally, this is made possible by some metaclass and descriptor magic instantiating
the FlexQuery
type whenever it is accessed as class attribute of a Manager
or QuerySet
object. The resulting FlexQuery
instance will be tied to its
owner and use that for all its filtering.
A FlexQuery
instance is directly callable (Fruit.objects.large()
), which just
applies the filters returned by our custom Q function to the base QuerySet
. This
is a well-known usage pattern you might have come across often when writing custom
Django model managers or querysets.
However, FlexQuery
also comes with an as_q()
method, which lets you access the
Q
object directly (Fruit.objects.filter(Fruit.objects.large.as_q())
). The
FlexQuery
can mediate between these two and deliver what you need in your
particular situation.
Conversion Costs¶
Providing a standalone QuerySet
filtered by the Q
from a supplied Q
function is a cheap operation. The Q
object generated by your custom function is
simply applied to the base using QuerySet.filter()
, resulting in a new QuerySet
you may either evaluate straight away or use to create a sub-query.
Why do I Need This?¶
This approach enables you to declare logic for filtering once with the Manager
or QuerySet
of the model it belongs to. When combined with the prefix() method
of the extended Q object implementation, related models can then simply fetch the
generated Q object and prefix it with the related field’s name in order to reuse it
in their own filtering code, without needing sub-queries. Think of something like:
class TreeQuerySet(QuerySet):
@FlexQuery.from_func
def having_ripe_apples(base):
return Q(kind="apple") & Fruid.objects.large.as_q().prefix("fruits")
API¶
This module provides a convenient way to declare custom filtering logic with Django’s model managers in a reusable fashion using Q objects.
-
class
django_flexquery.flexquery.
FlexQuery
(base)¶ Flexibly provides model-specific query constraints as an attribute of Manager or QuerySet objects.
When a sub-type of FlexQuery is accessed as class attribute of a Manager or QuerySet object, its metaclass, which is implemented as a descriptor, will automatically initialize and return an instance of the FlexQuery type bound to the holding Manager or QuerySet.
-
__call__
(*args, **kwargs)¶ Filters the base queryset using the provided function, relaying arguments.
- Returns QuerySet
-
as_q
(*args, **kwargs)¶ Returns the result of the configured function, relaying arguments.
- Returns Q
-
call_bound
(*args, **kwargs)¶ Calls the provided function with
self.base.all()
as first argument.This may be overwritten if arguments need to be preprocessed in some way before being passed to the custom function.
- Returns Q
-
classmethod
from_func
(func=None, **attrs)¶ Creates a
FlexQuery
sub-type from a function.This classmethod can be used as decorator. As long as
func
isNone
, afunctools.partial
with the given keyword arguments is returned.- Parameters
func (function | None) – function taking a base
QuerySet
and returning aQ
objectattrs – additional attributes to set on the newly created type
- Returns InitializedFlexQueryType | functools.partial
- Raises
TypeError – if
func
is no function
-
-
class
django_flexquery.flexquery.
Manager
¶ Use this class’
from_queryset
method if you want to derive aManager
from aQuerySet
withFlexQuery
members. If Django’s nativeManager.from_queryset
was used instead, those members would be lost.
-
class
django_flexquery.flexquery.
QuerySet
(model=None, query=None, using=None, hints=None)¶ Adds support for deriving a
Manager
from aQuerySet
class viaas_manager
, preservingFlexQuery
members.
contrib.user_based
¶
FlexQuery variant that restricts the base QuerySet for a given user.
-
class
django_flexquery.contrib.user_based.
UserBasedFlexQuery
(base)¶ This is a slightly modified
FlexQuery
implementation, accepting either adjango.http.HttpRequest
or a user object as argument for the custom function and passing the user through.When no user (or
None
) is given, the behavior is determined by theno_user_behavior
attribute, which may be set to one of the following constants defined on theUserBasedFlexQuery
class:NUB_ALL
: don’t restrict the querysetNUB_NONE
: restrict to the empty queryset (default)NUB_PASS
: let the custom function handle auser
ofNone
If the
pass_anonymous_user
attribute is changed toFalse
,django.contrib.auth.models.AnonymousUser
objects are treated as if they wereNone
and the configured no-user behavior comes to play.Because it can handle an
HttpRequest
directly, instances of thisFlexQuery
may also be used in conjunction with the django_filters library as thequeryset
parameter of filters.-
call_bound
(user, *args, **kwargs)¶ Calls the custom function with a user, followed by the remaining arguments.
- Parameters
user (django.contrib.auth.models.User | django.http.HttpRequest | None) – User to filter the queryset for
- Returns Q
This library aims to provide a new way of declaring reusable QuerySet filtering logic in your Django project, incorporating the DRY principle and maximizing user experience and performance by allowing you to decide between sub-queries and JOINs.
Its strengths are, among others:
Easy to learn in minutes
Cleanly integrates with Django’s ORM
Small code footprint, hard for bugs to hide - ~150 lines of code (LoC)
100% test coverage
Fully documented code, formatted using the excellent Black Code Formatter.
When referencing a related model in a database query, you usually have the choice
between using a JOIN (X.objects.filter(y__z=2)
) or performing a sub-query
(X.objects.filter(y__in=Y.objects.filter(z=2))
).
We don’t want to judge which one is better, because that depends on the concrete query and how the database engine in use optimizes it. In many cases, it will hardly make a noticeable difference at all. However, when the amount of data grows, doing queries right can save you and the users of your application several seconds, and that is what django-flexquery is for.
Requirements¶
Continuous integration ensures compatibility with Python 3.7 + Django 2.2 and 3.0.
Installation¶
pip install django-flexquery
No changes to your Django settings are required; no INSTALLED_APPS
, no
MIDDLEWARE_CLASSES
.