Есть ли способ дополнить django QuerySets дополнительными атрибутами? - PullRequest
3 голосов
/ 09 августа 2011

Я пытаюсь добавить некоторые дополнительные атрибуты к элементам QuerySet, чтобы я мог использовать дополнительную информацию в шаблонах, вместо того, чтобы обращаться к базе данных несколько раз.Позвольте мне проиллюстрировать это на примере, предполагая, что у нас есть Книги с ForeignKeys для авторов.

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

Поэтому добавление атрибутов к элементам QuerySet работает, если вы не используете такие вещи, как reverse ().

Мой текущий обходной путь - просто использовать select_related (), чтобы снова извлечь дополнительную информацию, которая мне нужна, из базы данных, даже если у меня уже есть память.

Есть ли лучший способ сделать это?

Ответы [ 5 ]

4 голосов
/ 19 октября 2015

Это старый вопрос, но я добавлю свое решение, потому что оно мне недавно понадобилось.

В идеале мы могли бы использовать какой-то прокси-объект для 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 может быть полезна, если вы хотите перейти к более полной реализации

2 голосов
/ 09 августа 2011

Ошибка возникает из-за того, что 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 из запроса, то вам лучше сделать один запрос, сохранить его в списке и, в конечном итоге, полностью изменить полученный список.

Если, с другой стороны, вы получаете «голову» и «очередь» таблицы, лучше сделать два запроса, потому что вы не будете запрашивать все «средние» бесполезные объекты.

0 голосов
/ 02 декабря 2017

Вот моя версия alter_items, предложенная Уиллом Харди.

Вместо одного значения он допускает разные значения настраиваемого атрибута для каждого объекта: вы можете передать сопоставление значений по идентификатору объекта.

Он также автоматически переносит результаты всех методов, которые возвращают 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
0 голосов
/ 09 августа 2011

Если вы итерируете набор запросов до .reverse, затем вызываете реверс и повторяете результирующий набор запросов снова, тогда будет выполнено 2 разных запроса SQL, метод .reverse () не будет сторнировать уже полученные результаты, он будет повторно извлекать (возможно, изменились) результаты с другим запросом SQL. То, что вы делаете, не только хрупко, но и неэффективно.

Чтобы избежать второго запроса SQL, вы можете либо изменить набор запросов перед его повторением, либо изменить список с экземплярами модели в python, используя, например, встроенная «обратная» функция (см. ответ MattoTodd).

0 голосов
/ 09 августа 2011

Я бы предположил, что вызов .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()
...