有没有办法用额外的属性来增强 django 查询集?

发布于 2024-11-29 02:46:49 字数 828 浏览 0 评论 0原文

我正在尝试向 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()之类的东西,向查询集的元素添加属性就可以了。

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

有更好的方法吗?

I'm trying to add some extra attributes to the elements of a QuerySet so I can use the extra information in templates, instead of hitting the database multiple times. Let me illustrate by example, assuming we have Books with ForeignKeys to Authors.

>>> 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.

So adding attributes to the elements of a QuerySet works, so long as you don't use things like reverse().

My current workaround is to just use select_related() to fetch the extra information I need from the database again, even though I already have in memory.

Is there a better way to do it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

本宫微胖 2024-12-06 02:46:50

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

理想情况下,我们可以为 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 可能会很有用

This is an old question, but I'll add my solution because I needed it recently.

Ideally we could use some sort of proxy object for the QuerySet. Our proxy version would then make the changes during iteration.

It will be hard to cover all possible scenarios, QuerySet objects are a little complicated and are used in many different ways. But for the simple case of adding an attribute at the last minute because sending to a template (or generic view), the following might work:

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

And then use this like so:

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

Because it is not a true proxy, you may need to make further modifications depending on how it is used, but this is the rough approach. It would be nice if there were an easy way to make a proxy class in Python with new style classes. The external library wrapt might be useful if you want to go with a more complete implementation

与君绝 2024-12-06 02:46:50

出现错误是因为 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 关键字可以复杂得多,事实上它可以是几乎任何可以适合的有意义的 SQL 片段 中的 [XXX]

SELECT ..., [XXX] as price, ... FROM ... WHERE ... (etc)

EDIT

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

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

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

The error arises because qs.reverse() give rise to a new QuerySet instance, so you are not reversing the old one.

If you want to have a base QS on which to act you can do the following:

>>> 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

Of course the select keyword can be much more complex, in fact it can be almost any any meaningful SQL snippet that could fit the [XXX] in

SELECT ..., [XXX] as price, ... FROM ... WHERE ... (etc)

EDIT

As pointed out in other responses, this solution may be inefficient.

If you are sure to get all the Book objects from the query, then you 'd better make one query, store it into a list and eventually reverse the resulting list.

If, on the other hand, you are getting the "head" and the "queue" of the table, making two queries is better, because you won't query all the "middle" useless objects.

喜你已久 2024-12-06 02:46:50

我假设在查询集上调用 .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()

I would assume calling .reverse on a queryset s what is causing your issues. try this:

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

reverse = books_set.reverse()
浅浅 2024-12-06 02:46:50

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

为了避免第二个 SQL 查询,您可以在迭代之前反转查询集,或者使用内置的“reversed”函数反转 python 中模型实例的列表(请参阅 MattoTodd 答案)。

If you iterate queryset before .reverse, then call the reverse and iterate the resulting queryset again then there will be 2 different SQL queries executed, .reverse() method won't reverse already fetched results, it will re-fetch the (possibly changed) results with an another SQL query. So what you're doing is not only fragile but also inefficient.

In order to avoid the second SQL query you can either reverse the queryset before iterating it or reverse a list with model instances in python using e.g. builtin 'reversed' function (see MattoTodd answer).

冷了相思 2024-12-06 02:46:50

这是 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

Here is my version of alter_items proposed by Will Hardy.

Instead of a single value it allows different values of a custom attribute for each object: you can pass a mapping of values by object id.

It also automatically wraps the results of all methods that return 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文