有没有办法用额外的属性来增加django QuerySets?

时间:2011-08-09 17:04:41

标签: python django django-queryset

我正在尝试向QuerySet的元素添加一些额外的属性,这样我就可以在模板中使用额外的信息,而不是多次访问数据库。让我举例说明一下,假设我们有“外国人与作者的书籍”。

>>> books = Book.objects.filter(author__id=1)
>>> for book in books:
...     book.price = 2  # "price" is not defined in the Book model
>>> # Check I can get back the extra information (this works in templates too):
>>> books[0].price
2
>>> # but it's fragile: if I do the following:
>>> reversed = books.reverse()
>>> reversed[0].price
Traceback (most recent call last):
  ...
AttributeError: 'Book' object has no attribute 'price'
>>> # i.e., the extra field isn't preserved when the QuerySet is reversed.

因此,只要不使用reverse()之类的东西,就可以在QuerySet的元素中添加属性。

我目前的解决方法是使用select_related()再次从数据库中获取我需要的额外信息,即使我已经在内存中。

有更好的方法吗?

5 个答案:

答案 0 :(得分:4)

这是一个老问题,但我会添加我的解决方案,因为我最近需要它。

理想情况下,我们可以为QuerySet使用某种代理对象。然后,我们的代理版本将在迭代期间进行更改。

很难涵盖所有可能的场景,QuerySet对象有点复杂,并且以多种不同的方式使用。但是对于在最后一刻添加属性的简单情况,因为发送到模板(或通用视图),以下可能有效:

class alter_items(object):

  def __init__(self, queryset, **kwargs):
    self.queryset = queryset
    self.kwargs = kwargs

  # This function is required by generic views, create another proxy
  def _clone(self):
    return alter_items(queryset._clone(), **self.kwargs)

  def __iter__(self):
    for obj in self.queryset:
      for key, val in self.kwargs.items():
        setattr(obj, key, val)
      yield obj

然后像这样使用它:

query = alter_items(Book.objects.all(), price=2)

因为它不是真正的代理,您可能需要根据其使用方式进行进一步修改,但这是粗略的方法。如果有一种简单的方法可以使用新的样式类在Python中创建代理类,那将是很好的。如果您想要更完整的实现,外部库wrapt可能会很有用

答案 1 :(得分:2)

出现错误是因为qs.reverse()会产生一个新的QuerySet实例,所以你不会反转旧的。

如果您想要有一个基本QS可以采取行动,您可以执行以下操作:

>>> augmented_books = Book.objects.extra(select={'price': 2))
>>> augmented_books[0].price
2
>>> augmented_books_rev = augmented_books.reverse()
>>> augmented_books_rev[0].price
2

当然select关键字可能要复杂得多,事实上它几乎可以是任何适合[XXX]

的SQL片段。
SELECT ..., [XXX] as price, ... FROM ... WHERE ... (etc)

修改

正如其他回复中所指出的,此解决方案可能效率低下。

如果您确定从查询中获取全部 Book对象,那么您最好进行一次查询,将其存储到列表中并最终反转结果列表。

另一方面,如果您获得表的“头部”和“队列”,则进行两次查询会更好,因为您不会查询所有“中间”无用的对象。

答案 2 :(得分:0)

我会假设在查询集上调用.reverse导致问题的原因。试试这个:

books = Book.objects.filter(author__id=1)
books_set = []
for book in books:
    book.price = 2
    books_set.append(book)

reverse = books_set.reverse()

答案 3 :(得分:0)

如果在.reverse之前迭代查询集,则调用反向并再次迭代生成的查询集,然后执行2个不同的SQL查询,.reverse()方法将不会反转已经获取的结果,它将重新获取(可能已更改)结果与另一个SQL查询。所以你所做的不仅是脆弱而且效率低下。

为了避免第二个SQL查询,您可以在迭代之前反转查询集,或者使用例如python中的模型实例反转列表。内置'逆转'功能(见MattoTodd回答)。

答案 4 :(得分:0)

以下是Will Hardy提出的alter_items版本。

它不是单个值,而是为每个对象提供不同的自定义属性值:您可以按对象ID传递值的映射。

它还会自动包装返回QuerySet的所有方法的结果。

import types
from itertools import islice

from django.db.models import Model, QuerySet


class QuerySetWithCustomAttributes(object):
    def __init__(self, queryset, **custom_attrs):
        self.queryset = queryset
        self.custom_attrs = custom_attrs

    def __set_custom_attrs(self, obj):
        if isinstance(obj, Model):
            for key, val in self.custom_attrs.items():
                setattr(obj, key, val[obj.id])
        return obj

    def __iter__(self):
        for obj in self.queryset:
            yield self.__set_custom_attrs(obj)

    def __getitem__(self, ndx):
        if type(ndx) is slice:
            return self.__class__(self.queryset.__getitem__(ndx), **self.custom_attrs)
        else:
            return self.__set_custom_attrs(next(islice(self.queryset, ndx, ndx + 1)))

    def __len__(self):
        return len(self.queryset)

    def __apply(self, method):
        def apply(*args, **kwargs):
            result = method(*args, **kwargs)
            if isinstance(result, QuerySet):
                result = self.__class__(result, **self.custom_attrs)
            elif isinstance(result, Model):
                self.__set_custom_attrs(result)
            return result
        return apply

    def __getattr__(self, name):
        attr = getattr(self.queryset, name)
        if isinstance(attr, types.MethodType):
            return self.__apply(attr)
        else:
            return attr