1
0
mirror of https://github.com/django/django.git synced 2025-03-28 18:20:49 +00:00
2014-01-11 13:16:01 +02:00

242 lines
9.1 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

==============
Custom lookups
==============
.. module:: django.db.models.lookups
:synopsis: Custom lookups
.. currentmodule:: django.db.models
Django's ORM works using lookup paths when building query filters and other
query conditions. For example in the query Book.filter(author__age__lte=30)
the part "author__age__lte" is the lookup path.
The lookup path consist of three different parts. First is the related
lookups. In the author__age__lte example the part author refers to Book's
related model Author. Second part of the lookup path is the field. This is
Author's age field in the example. Finally the lte part is commonly called
just lookup. Both the related lookups part and the final lookup part can
contain multiple parts, for example "author__friends__birthdate__year__lte"
has author, friends as related lookups, birthdate as the field and year, lte
as final lookup part.
This documentation concentrates on writing custom lookups. By writing custom
lookups it is possible to control how Django interprets the final lookup part.
Django will fetch a ``Lookup`` class from the final field using the field's
method get_lookup(lookup_name). This method is allowed to do these things:
1. Return a Lookup class
2. Raise a FieldError
3. Return None
Returning None is only available during backwards compatibility period.
The interpretation is to use the old way of lookup hadling inside the ORM.
The Lookup class
~~~~~~~~~~~~~~~~
A Lookup operates on two values and produces boolean results. The values
are called lhs and rhs. The lhs is usually a field reference, but it can be
anything implementing the query expression API. The rhs is a value to compare
against.
The API is as follows:
.. attribute:: lookup_name
A string used by Django to distinguish different lookups. For example
'exact'.
.. method:: __init__(lhs, rhs)
The lhs is something implementing the query expression API. For example in
author__age__lte=30 the lhs is a Col instance referencing the age field of
author model. The rhs is the value to compare against. It can be Python value
(30 in the example) or SQL reference (produced by using F() or queryset for
example).
.. attribute:: Lookup.lhs
The left hand side part of this lookup. You can assume it implements the
query expression interface.
.. attribute:: Lookup.rhs
The value to compare against.
.. method:: Lookup.process_lhs(qn, connection)
Turns the lhs into query string + params.
.. method:: Lookup.process_rhs(qn, connection)
Turns the rhs into query string + params.
.. method:: Lookup.as_sql(qn, connection)
This method is used to produce the query string of the Lookup. A typical
implementation is usually something like::
def as_sql(self, qn, connection):
lhs, params = self.process_lhs(qn, connection)
rhs, rhs_params = self.process_rhs(qn, connection)
params = lhs_params.extend(rhs_params)
return '%s <OPERATOR> %s', (lhs, rhs), params
where the <OPERATOR> is some query operator. The qn is a callable that
can be used to convert strings to quoted variants (that is, colname to
"colname"). Note that the quotation is *not* safe against SQL injection.
In addition the qn implements method compile() which can be used to turn
anything with as_sql() method to query string. You should always call
qn.compile(part) instead of part.as_sql(qn, connection) so that 3rd party
backends have ability to customize the produced query string. More of this
later on.
The connection is the connection the SQL is compiled against.
In addition the Lookup class has some private methods - that is, implementing
just the above mentioned attributes and methods is not enough, instead you
must subclass Lookup.
The Extract class
~~~~~~~~~~~~~~~~~
An Extract is something that converts a value to another value in the query
string. For example you could have an Extract that procudes modulo 3 of the
given value. In SQL this is something like "author"."age" % 3.
Extracts are used in nested lookups. The Extract class must implement the
query part interface.
Extracts should be written by subclassing django.db.models.Extract.
A simple Lookup example
~~~~~~~~~~~~~~~~~~~~~~~
This is how to write a simple mod3 lookup for IntegerField::
from django.db.models import Lookup, IntegerField
class Mod3(Lookup):
lookup_name = 'mod3'
def as_sql(self, qn, connection):
lhs_sql, params = self.process_lhs(qn, connection)
rhs_sql, rhs_params = self.process_rhs(qn, connection)
params.extend(rhs_params)
# We need doulbe-escaping for the %%%% operator.
return '%s %%%% %s' % (lhs_sql, rhs_sql), params
IntegerField.register_lookup(Div3)
Now all IntegerFields or subclasses of IntegerField will have
a mod3 lookup. For example you could do Author.objects.filter(age__mod3=2).
This query would return every author whose age % 3 == 2.
A simple nested lookup example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is how to write an Extract and a Lookup for IntegerField. The example
lookup can be used similarly as the above mod3 lookup, and in addition it
support nesting lookups::
class Mod3Extract(Extract):
lookup_name = 'mod3'
def as_sql(self, qn, connection):
lhs, lhs_params = qn.compile(self.lhs)
return '%s %%%% 3' % (lhs,), lhs_params
IntegerField.register_lookup(Mod3Extract)
Note that if you already added Mod3 for IntegerField in the above
example, now Mod3Extract will override that lookup.
This lookup can be used like Mod3 lookup, but in addition it supports
nesting, too. The default output type for Extracts is the same type as the
lhs' output_type. So, the Mod3Extract supports all the same lookups as
IntegerField. For example Author.objects.filter(age__mod3__in=[1, 2])
returns all authors for which age % 3 in (1, 2).
A more complex nested lookup
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We will write a Year lookup that extracts year from date field. This
field will convert the output type of the field - the lhs (or "input")
field is DateField, but output is of type IntegerField.::
from django.db.models import IntegerField, DateField
from django.db.models.lookups import Extract
class YearExtract(Extract):
lookup_name = 'year'
def as_sql(self, qn, connection):
lhs_sql, params = qn.compile(self.lhs)
# hmmh - this is internal API...
return connection.ops.date_extract_sql('year', lhs_sql), params
@property
def output_type(self):
return IntegerField()
DateField.register_lookup(YearExtract)
Now you could write Author.objects.filter(birthdate__year=1981). This will
produce SQL like 'EXTRACT('year' from "author"."birthdate") = 1981'. The
produces SQL depends on used backend. In addtition you can use any lookup
defined for IntegerField, even div3 if you added that. So,
Authos.objects.filter(birthdate__year__div3=2) will return every author
with birthdate.year % 3 == 2.
We could go further and add an optimized implementation for exact lookups::
from django.db.models.lookups import Lookup
class YearExtractOptimized(YearExtract):
def get_lookup(self, lookup):
if lookup == 'exact':
return YearExact
return super(YearExtractOptimized, self).get_lookup()
class YearExact(Lookup):
def as_sql(self, qn, connection):
# We will need to skip the extract part, and instead go
# directly with the originating field, that is self.lhs.lhs
lhs_sql, lhs_params = self.process_lhs(qn, connection, self.lhs.lhs)
rhs_sql, rhs_params = self.process_rhs(qn, connection)
# Note that we must be careful so that we have params in the
# same order as we have the parts in the SQL.
params = []
params.extend(lhs_params)
params.extend(rhs_params)
params.extend(lhs_params)
params.extend(rhs_params)
# We use PostgreSQL specific SQL here. Note that we must do the
# conversions in SQL instead of in Python to support F() references.
return ("%(lhs)s >= (%(rhs)s || '-01-01')::date "
"AND %(lhs)s <= (%(rhs)s || '-12-31')::date" %
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
Note that we used PostgreSQL specific SQL above. What if we want to support
MySQL, too? This can be done by registering a different compiling implementation
for MySQL::
from django.db.backends.utils import add_implementation
@add_implementation(YearExact, 'mysql')
def mysql_year_exact(node, qn, connection):
lhs_sql, lhs_params = node.process_lhs(qn, connection, node.lhs.lhs)
rhs_sql, rhs_params = node.process_rhs(qn, connection)
params = []
params.extend(lhs_params)
params.extend(rhs_params)
params.extend(lhs_params)
params.extend(rhs_params)
return ("%(lhs)s >= str_to_date(concat(%(rhs)s, '-01-01'), '%%%%Y-%%%%m-%%%%d') "
"AND %(lhs)s <= str_to_date(concat(%(rhs)s, '-12-31'), '%%%%Y-%%%%m-%%%%d')" %
{'lhs': lhs_sql, 'rhs': rhs_sql}, params)
Now, on MySQL instead of calling as_sql() of the YearExact Django will use the
above compile implementation.