¿Hay alguna manera de boost django QuerySets con atributos adicionales?

Estoy tratando de agregar algunos atributos adicionales a los elementos de un QuerySet para poder usar la información adicional en las plantillas, en lugar de golpear la base de datos varias veces. Permítanme ilustrar con el ejemplo, asumiendo que tenemos libros con ForeignKeys para autores.

>>> 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' >>> # ie, the extra field isn't preserved when the QuerySet is reversed. 

Por lo tanto, agregar atributos a los elementos de un QuerySet funciona, siempre y cuando no uses elementos como reverse ().

Mi solución actual es usar solo select_related () para recuperar la información adicional que necesito de la base de datos nuevamente, aunque ya la tengo en la memoria.

Hay una mejor manera de hacerlo?

El error surge porque qs.reverse () da lugar a una nueva instancia de QuerySet, por lo que no está invirtiendo la anterior.

Si desea tener un QS base en el que actuar, puede hacer lo siguiente:

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

Por supuesto, la palabra clave select puede ser mucho más compleja, de hecho, puede ser casi cualquier fragmento de código SQL significativo que pueda encajar en [XXX]

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

EDITAR

Como se señaló en otras respuestas, esta solución puede ser ineficiente.

Si está seguro de obtener todos los objetos de Libro de la consulta, entonces debería hacer una consulta, almacenarla en una lista y eventualmente revertir la lista resultante.

Si, por otro lado, está obteniendo el “encabezado” y la “cola” de la tabla, es mejor hacer dos consultas, porque no consultará todos los objetos inútiles “intermedios”.

Esta es una pregunta antigua, pero agregaré mi solución porque la necesité recientemente.

Lo ideal sería utilizar algún tipo de objeto proxy para el QuerySet . Nuestra versión proxy haría los cambios durante la iteración.

Será difícil cubrir todos los escenarios posibles, los objetos QuerySet son un poco complicados y se utilizan de muchas maneras diferentes. Pero para el caso simple de agregar un atributo en el último minuto porque enviar a una plantilla (o vista genérica), podría funcionar lo siguiente:

 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 

Y luego usa esto así:

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

Debido a que no es un verdadero proxy, es posible que deba realizar más modificaciones según cómo se use, pero este es el enfoque aproximado. Sería bueno si hubiera una forma fácil de crear una clase proxy en Python con nuevas clases de estilo. El wrapt biblioteca externa puede ser útil si desea ir con una implementación más completa

Asumiría que llamar a .reverse en una consulta es lo que está causando sus problemas. prueba esto:

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

Si itera queryset antes de .reverse, llame al revés e itere el queryset resultante de nuevo, entonces se ejecutarán 2 consultas SQL diferentes, el método .reverse () no revertirá los resultados ya recuperados, volverá a recuperar el (posiblemente cambiado) ) resultados con otra consulta SQL. Entonces, lo que estás haciendo no solo es frágil sino también ineficiente.

Para evitar la segunda consulta SQL, puede revertir el conjunto de consultas antes de iterarlo o revertir una lista con instancias de modelos en python usando, por ejemplo, la función ‘invertida’ incorporada (consulte la respuesta de MattoTodd).

Aquí está mi versión de alter_items propuesta por Will Hardy.

En lugar de un solo valor, permite diferentes valores de un atributo personalizado para cada objeto: puede pasar una asignación de valores por ID de objeto.

También envuelve automáticamente los resultados de todos los métodos que devuelven 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