在 Django 中,当过滤查询集时,提供了一组可用的查找条件,如 __contains、__iexact、__in 等。如果需要为管理器提供新的查找条件,以便能够使用类似以下查询语句:
twentysomethings = Person.objects.filter(age__within5=25)
该查询语句能返回年龄介于 20 到 30 之间的全部 Person 对象。如何实现此类需求?是否需要对 QuerySet 或 Manager 类进行子类化来完成此操作?具体实现方式如何?
2、解决方案
方法一:自定义 QuerySet 和 Manager
一个更加灵活的做法是编写自定义的 QuerySet 和 Manager。以下是具体实现代码:
class PersonQuerySet(models.query.QuerySet):
def in_age_range(self, min, max):
return self.filter(age__gte=min, age__lt=max)
class PersonManager(models.Manager):
def get_query_set(self):
return PersonQuerySet(self.model)
def __getattr__(self, name):
return getattr(self.get_query_set(), name)
class Person(models.Model):
age = #...
objects = PersonManager()
通过该方法可以实现自定义查询的链式调用。例如:
Person.objects.in_age_range(20,30)
Person.objects.exclude(somefield = some_value).in_age_range(20, 30)
方法二:创建 Manager 方法
最佳实践是创建 Manager 方法,而不是创建字段查找条件。示例代码如下:
class PersonManger(models.Manager):
def in_age_range(self, min, max):
return self.filter(age__gte=min, age__lt=max)
class Person(models.Model):
age = #...
objects = PersonManager()
使用方法如下:
twentysomethings = Person.objects.in_age_range(20, 30)
方法三:子类化 QuerySet 并重写 _filter_or_exclude() 方法
首先需要说明的是,Django 中并没有公开的机制可以帮助实现该需求。但如果确实需要实现此功能,可以考虑子类化 QuerySet 并重写 _filter_or_exclude() 方法。然后创建一个自定义的管理器,仅返回自定义的 QuerySet(或使用不推荐的猴补方式修改 Django 的 QuerySet)。以下是在这方面的一个尝试:
class PersonQuerySet(models.query.QuerySet):
def _filter_or_exclude(self, negate, *args, **kwargs):
cust_lookups = filter(lambda s: s[0].endswith('__within5'), kwargs.items())
for lookup in cust_lookups:
kwargs.pop(lookup[0])
lookup_prefix = lookup[0].rsplit('__',1)[0]
kwargs.update({lookup_prefix + '__gte':lookup[1]-5,
lookup_prefix + '__lt':lookup[1]+5})
return super(PersonQuerySet, self)._filter_or_exclude(negate, *args, **kwargs)
class PersonManager(models.Manager):
def get_query_set(self):
return PersonQuerySet(self.model)
class Person(models.Model):
age = #...
objects = PersonManager()
方法四:使用 Django 1.7 提供的解决方案
从 Django 1.7 开始,提供了一种简单的方法来实现自定义字段查找条件。以下是如何实现示例中需求的代码:
from django.db.models import Lookup
class AbsoluteValueLessThan(Lookup):
lookup_name = 'lt'
def as_sql(self, qn, connection):
lhs, lhs_params = qn.compile(self.lhs.lhs)
rhs, rhs_params = self.process_rhs(qn, connection)
params = lhs_params + rhs_params + lhs_params + rhs_params
return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params
AbsoluteValue.register_lookup(AbsoluteValueLessThan)
在注册时,可以使用 Field.register_lookup(AbsoluteValueLessThan) 代替。