自定义查找

Django提供了各种各样的ref:用于过滤的内置查找(例如,“exact”和“icontains”)。 本文档解释了如何编写自定义查找以及如何更改已有查找的工作方式。 有关lookup的API参考,请参阅:doc:/ ref / models / lookups

一个简单的查找示例

让我们从一个简单的自定义查找开始。我们编写一个自定义查找 ne ,它与 exact 相反。Author.objects.filter(name__ne='Jack') 将会转换成 SQL:

  1. "author"."name" <> 'Jack'

SQL 会自动适配不同的后端, 所以我们不需要对使用不同的数据库担心.

完成此工作需要两个步骤。第一首先我们需要实现查找,第二我们需要将它告知Django。 查找的实现非常简单:

  1. from django.db.models import Lookup
  2.  
  3. class NotEqual(Lookup):
  4. lookup_name = 'ne'
  5.  
  6. def as_sql(self, compiler, connection):
  7. lhs, lhs_params = self.process_lhs(compiler, connection)
  8. rhs, rhs_params = self.process_rhs(compiler, connection)
  9. params = lhs_params + rhs_params
  10. return '%s <> %s' % (lhs, rhs), params

要注册NotEqual查找,我们只需要在我们希望查找可用的字段类上调用register_lookup方法。 在这种情况下,查找对所有Field子类都有意义,所以我们直接用Field注册它:

  1. from django.db.models.fields import Field
  2. Field.register_lookup(NotEqual)

查找注册也可以用修饰模式来完成

  1. from django.db.models.fields import Field
  2.  
  3. @Field.register_lookup
  4. class NotEqualLookup(Lookup):
  5. # ...

现在我们可以用“foo__ne”来代表“foo”的任意字段。你需要确保在创建任意的queryset之前使用它。你可以在“models.py”文件内设置它,或者在“AppConfig”内使用“ready()”方法注册它。

仔细观察实现过程,最开始我们需要“lookupname”这个属性。这个可以保证ORM理解如何编译“namene”和使用“NotEqual”来建立结构化查询语言SQL。按照惯例,这些名字“name_ne”是小写字母字符串,但是很麻烦的是必须有“”字符串

之后我们需要定义一个“as_sql”方法。这方法需要一个“SQLCompiler” 对象, 被叫做编译器,和一个有效的数据库连接。“SQLCompller”对象没有文档,我们只需要知道它有一个compile()方法可以返回一个元组包括SQL字符串,和插入这个字符串的参数。大部分情况下,你不需要直接使用这个对象你可以把它传送给“process_lhs()”和“process_rhs()”

“Lookup”工作依靠两个值, “lhs”和“rhs”,代表左右手边,左手是一个字段参考,但它可以是任何来解释ref:应用程序的检索表达。右手是一个用户给的数值。举个例子:Author.objects.filter(name__ne='Jack'),左手是一个参考对应着Author模型的名字,“Jack”是右手端。

我们调用“process_lhs”和“process_rhs”转化他们成为我们想要的用来检索的值通过之前我们提到的“编译器”。这个方法返回一个元组包含SQL数据库和插入SQL数据库一些参数,刚好就是我们‘as_sql’需要返回的。使用前面的例子,“process_lhs”返回(&#39;&#34;author&#34;.&#34;name&#34;&#39;, []) ,“process_lhs”返回(&#39;&#34;%s&#34;&#39;, [&#39;Jack&#39;]).在这个例子里面没有左手边的参数,但是这需要看情况而定,我们还需要包括这些参数当我们返回的时候。

最后,我们将这些部分组合成一个带有&lt;&gt;的SQL表达式,并提供查询的所有参数。 然后我们返回一个包含生成的SQL字符串和参数的元组。

让我们举一个简单的转换器示例

上面的自定义查找没问题,但在某些情况下,您可能希望能够将一些查找链接在一起。 例如,假设我们正在构建一个我们想要制作一个带有abs()运算符的应用程序。 我们有一个Experiment模型,它记录起始值,结束值和变化(开始 - 结束)。 我们想找到所有在Experiment模型中change属性等于一定数量的(Experiment.objects.filter(changeabs = 27)),或者在Experiment模型中change属性没有超过一定数量的(Experiment.objects.filter(changeabs__lt= 27))。

注解

这个例子有点刻意,但它很好地演示了以数据库后端独立方式可能实现的功能范围,并且没有重复Django中的功能

我们将从编写一个AbsoluteValue变换器开始。 这将使用SQL中的ABS()函数在比较进行之前首先转换值:

  1. from django.db.models import Transform
  2.  
  3. class AbsoluteValue(Transform):
  4. lookup_name = 'abs'
  5. function = 'ABS'

下一步, 让我们为其注册 IntrgerField:

  1. from django.db.models import IntegerField
  2. IntegerField.register_lookup(AbsoluteValue)

我们现在可以运行之前的查询。 ``Experiment.objects.filter(change__abs = 27)``将生成以下SQL

  1. SELECT ... WHERE ABS("experiments"."change") = 27

通过使用Transform而不是Lookup,这意味着我们可以在之后链接进一步的查找。 所以Experiment.objects.filter(change__abs__lt = 27)将生成以下SQL

  1. SELECT ... WHERE ABS("experiments"."change") < 27

请注意,如果没有指定其他查找定义,Django则会将change__abs = 27解析为change__abs__exact = 27

这也允许结果用于ORDER BYDISTINCT ON子句。 例如Experiment.objects.order_by(&#39;change__abs&#39;)会生成:

  1. SELECT ... ORDER BY ABS("experiments"."change") ASC

在支持字段去重的数据库(例如PostgreSQL)上,语句Experiment.objects.distinct(&#39;change__abs&#39;)会生成:

  1. SELECT ... DISTINCT ON ABS("experiments"."change")

Changed in Django 2.1:
上两段所提到的排序与去重的支持被加入了。

当我们在应用Transform之后查找允许哪些查找执行时,Django使用output_field属性。 我们不需要在这里指定它,因为它没有改变,但假设我们将AbsoluteValue应用于某个字段,该字段表示更复杂的类型(例如,相对于原点的点或复数) 那么我们可能想要指定转换返回一个FloatField类型以进行进一步的查找。 这可以通过在变换中添加output_field属性来完成:

  1. from django.db.models import FloatField, Transform
  2.  
  3. class AbsoluteValue(Transform):
  4. lookup_name = 'abs'
  5. function = 'ABS'
  6.  
  7. @property
  8. def output_field(self):
  9. return FloatField()

这确保了像abs__lte这样的进一步查找与对FloatField一致。

编写一个高效的 abs__lt 查找

当使用上面写的abs查找时,生成的SQL在某些情况下不会有效地使用索引。 特别是,当我们使用change__abs__lt = 27时,这相当于change__gt = -27change__lt = 27。 (对于lte情况,我们可以使用SQLBETWEEN)。

因此, 我们希望 Experiment.objects.filter(changeabslt=27) 能生成以下 SQL:

  1. SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

实现方式是:

  1. from django.db.models import Lookup
  2.  
  3. class AbsoluteValueLessThan(Lookup):
  4. lookup_name = 'lt'
  5.  
  6. def as_sql(self, compiler, connection):
  7. lhs, lhs_params = compiler.compile(self.lhs.lhs)
  8. rhs, rhs_params = self.process_rhs(compiler, connection)
  9. params = lhs_params + rhs_params + lhs_params + rhs_params
  10. return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params
  11.  
  12. AbsoluteValue.register_lookup(AbsoluteValueLessThan)

这里有几件值得注意的事情。 首先,AbsoluteValueLessThan没有调用process_lhs()。 相反,它会跳过由AbsoluteValue完成的lhs的转换,并使用原始的lhs。 也就是说,我们希望得到&#34;experiments&#34;.&#34;change&#34;<code>而不是ABS(“实验”。“改变”)。 直接引用self.lhs.lhs是安全的,因为AbsoluteValueLessThan只能从AbsoluteValue查找访问,即lhs总是`AbsoluteValue的实例`。

另请注意,由于在查询中多次使用双方,所以需要多次包含“lhs_params”和“rhs_params”的参数。

最后的查询直接在数据库中进行反转( 27-27 )。 这样做的原因是,如果 self.rhs 不是普通的整数值(例如 F() 引用),我们就不能在Python中进行转换。

注解

In fact, most lookups with __abs could be implemented as range querieslike this, and on most database backends it is likely to be more sensible todo so as you can make use of the indexes. However with PostgreSQL you maywant to add an index on abs(change) which would allow these queries tobe very efficient.

A bilateral transformer example

The AbsoluteValue example we discussed previously is a transformation whichapplies to the left-hand side of the lookup. There may be some cases where youwant the transformation to be applied to both the left-hand side and theright-hand side. For instance, if you want to filter a queryset based on theequality of the left and right-hand side insensitively to some SQL function.

Let's examine the simple example of case-insensitive transformation here. Thistransformation isn't very useful in practice as Django already comes with a bunchof built-in case-insensitive lookups, but it will be a nice demonstration ofbilateral transformations in a database-agnostic way.

We define an UpperCase transformer which uses the SQL function UPPER() totransform the values before comparison. We definebilateral = True to indicate thatthis transformation should apply to both lhs and rhs:

  1. from django.db.models import Transform
  2.  
  3. class UpperCase(Transform):
  4. lookup_name = 'upper'
  5. function = 'UPPER'
  6. bilateral = True

下一步, 让我们注册它:

  1. from django.db.models import CharField, TextField
  2. CharField.register_lookup(UpperCase)
  3. TextField.register_lookup(UpperCase)

现在, 这个 ``Author.objects.filter(name__upper="doe")``查询集会生成一个像这样的不区分大小写的查询:

  1. SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

为现有的查找编写代替实现

Sometimes different database vendors require different SQL for the sameoperation. For this example we will rewrite a custom implementation forMySQL for the NotEqual operator. Instead of <> we will be using !=operator. (Note that in reality almost all databases support both, includingall the official databases supported by Django).

我们可以通过使用 as_mysql 方法创建 NotEqual 的子类来更改特定后端的行为:

  1. class MySQLNotEqual(NotEqual):
  2. def as_mysql(self, compiler, connection, **extra_context):
  3. lhs, lhs_params = self.process_lhs(compiler, connection)
  4. rhs, rhs_params = self.process_rhs(compiler, connection)
  5. params = lhs_params + rhs_params
  6. return '%s != %s' % (lhs, rhs), params
  7.  
  8. Field.register_lookup(MySQLNotEqual)

We can then register it with Field. It takes the place of the originalNotEqual class as it has the same lookup_name.

When compiling a query, Django first looks for as_%s % connection.vendormethods, and then falls back to as_sql. The vendor names for the in-builtbackends are sqlite, postgresql, oracle and mysql.

How Django determines the lookups and transforms which are used

In some cases you may wish to dynamically change which Transform orLookup is returned based on the name passed in, rather than fixing it. Asan example, you could have a field which stores coordinates or an arbitrarydimension, and wish to allow a syntax like .filter(coords__x7=4) to returnthe objects where the 7th coordinate has value 4. In order to do this, youwould override get_lookup with something like:

  1. class CoordinatesField(Field):
  2. def get_lookup(self, lookup_name):
  3. if lookup_name.startswith('x'):
  4. try:
  5. dimension = int(lookup_name[1:])
  6. except ValueError:
  7. pass
  8. else:
  9. return get_coordinate_lookup(dimension)
  10. return super().get_lookup(lookup_name)

You would then define get_coordinate_lookup appropriately to return aLookup subclass which handles the relevant value of dimension.

There is a similarly named method called get_transform(). get_lookup()should always return a Lookup subclass, and get_transform() aTransform subclass. It is important to remember that Transformobjects can be further filtered on, and Lookup objects cannot.

When filtering, if there is only one lookup name remaining to be resolved, wewill look for a Lookup. If there are multiple names, it will look for aTransform. In the situation where there is only one name and a Lookupis not found, we look for a Transform and then the exact lookup on thatTransform. All call sequences always end with a Lookup. To clarify:

  • .filter(myfield__mylookup) 将会调用 myfield.get_lookup('mylookup').
  • .filter(myfieldmytransformmylookup) 将会调用 myfield.get_transform('mytransform'), 接着调用 mytransform.get_lookup('mylookup').
  • .filter(myfield__mytransform) will first callmyfield.get_lookup('mytransform'), which will fail, so it will fall backto calling myfield.get_transform('mytransform') and thenmytransform.get_lookup('exact').