Django: Paginator + requête SQL brute
J'utilise Django Paginator partout sur mon site web et j'ai même écrit un template tag spécial, pour le rendre plus pratique. Mais maintenant je suis arrivé à un état, où j'ai besoin de faire une requête SQL brute personnalisée complexe, que sans un LIMIT
retournera environ 100k enregistrements.
comment utiliser Django Pagintor avec custom query?
exemple Simplifié de mon problème:
Mon modèle:
class PersonManager(models.Manager):
def complicated_list(self):
from django.db import connection
#Real query is much more complex
cursor.execute("""SELECT * FROM `myapp_person`""");
result_list = []
for row in cursor.fetchall():
result_list.append(row[0]);
return result_list
class Person(models.Model):
name = models.CharField(max_length=255);
surname = models.CharField(max_length=255);
age = models.IntegerField();
objects = PersonManager();
la façon dont J'utilise la pagination avec Django ORM:
all_objects = Person.objects.all();
paginator = Paginator(all_objects, 10);
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
try:
persons = paginator.page(page)
except (EmptyPage, InvalidPage):
persons = paginator.page(paginator.num_pages)
de cette façon, Django devient très intelligent, et ajoute LIMIT
pour une requête lors de l'exécution. Mais quand j'utilise custom manager:
all_objects = Person.objects.complicated_list();
toutes les données sont sélectionnées, et seulement alors la liste python est découpée, ce qui est très lent. Comment puis-je faire en sorte que mon custom manager se comporte de la même manière qu'un gestionnaire intégré?
4 réponses
en Regardant Paginator du code source fonction page () en particulier, je pense qu'il ne s'agit que d'implémenter découpage de votre côté, et traduire cela en clause limite pertinente dans la requête SQL. Vous pourriez aussi avoir besoin d'ajouter un peu de cache, mais cela commence à ressembler à QuerySet, donc peut-être que vous pouvez faire quelque chose d'autre:
- vous pouvez créer une vue de la base de données en utilisant CREATE VIEW myview comme [votre requête];
- ajouter le modèle Django pour cette vue, Meta: managed=False
- utilisez ce modèle comme tout autre modèle, y compris découper ses querysets - cela signifie qu'il est parfaitement adapté pour utiliser avec Paginator
(pour votre information - j'utilise cette approche depuis longtemps maintenant, même avec des relations complexes de plusieurs à plusieurs avec des vues imitant des tables intermédiaires m2m.)
Je ne sais pas pour Django 1.1 mais si vous pouvez attendre 1.2 (ce qui ne devrait plus être si long) vous pouvez faire usage de objects.raw()
comme décrit dans cet article et documentation de développement.
Sinon, si votre requête n'est pas trop complexe, peut-être en utilisant le extra
clause est suffisant.
Voici un RawPaginator
classe j'ai fait que des remplacements Paginator
pour travailler avec les premières requêtes. Elle prend un argument supplémentaire, count
, qui est le nombre total de votre requête. Il n'a pas tranche sur le object_list
parce que vous devez paginer dans votre première requête via OFFSET
et LIMIT
.
from django.core.paginator import Paginator
class RawPaginator(Paginator):
def __init__(self, object_list, per_page, count, **kwargs):
super().__init__(object_list, per_page, **kwargs)
self.raw_count = count
def _get_count(self):
return self.raw_count
count = property(_get_count)
def page(self, number):
number = self.validate_number(number)
return self._get_page(self.object_list, number, self)
je voulais aussi brancher un PaginatedRawQuerySet
ce que j'ai écrit (considérez ceci comme une version alpha). Cela ajoute la capacité de tranchage à un queryset brut. Veuillez vous référer pour cette réponse - que j'ai écrit pour une autre question avec une exigence similaire – afin de comprendre comment cela fonctionne (en particulier la section "un mot de prudence" à la fin).
from django.db import models
from django.db.models import sql
from django.db.models.query import RawQuerySet
class PaginatedRawQuerySet(RawQuerySet):
def __init__(self, raw_query, **kwargs):
super(PaginatedRawQuerySet, self).__init__(raw_query, **kwargs)
self.original_raw_query = raw_query
self._result_cache = None
def __getitem__(self, k):
"""
Retrieves an item or slice from the set of results.
"""
if not isinstance(k, (slice, int,)):
raise TypeError
assert ((not isinstance(k, slice) and (k >= 0)) or
(isinstance(k, slice) and (k.start is None or k.start >= 0) and
(k.stop is None or k.stop >= 0))), \
"Negative indexing is not supported."
if self._result_cache is not None:
return self._result_cache[k]
if isinstance(k, slice):
qs = self._clone()
if k.start is not None:
start = int(k.start)
else:
start = None
if k.stop is not None:
stop = int(k.stop)
else:
stop = None
qs.set_limits(start, stop)
return qs
qs = self._clone()
qs.set_limits(k, k + 1)
return list(qs)[0]
def __iter__(self):
self._fetch_all()
return iter(self._result_cache)
def count(self):
if self._result_cache is not None:
return len(self._result_cache)
return self.model.objects.count()
def set_limits(self, start, stop):
limit_offset = ''
new_params = tuple()
if start is None:
start = 0
elif start > 0:
new_params += (start,)
limit_offset = ' OFFSET %s'
if stop is not None:
new_params = (stop - start,) + new_params
limit_offset = 'LIMIT %s' + limit_offset
self.params = self.params + new_params
self.raw_query = self.original_raw_query + limit_offset
self.query = sql.RawQuery(sql=self.raw_query, using=self.db, params=self.params)
def _fetch_all(self):
if self._result_cache is None:
self._result_cache = list(super().__iter__())
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.model.__name__)
def __len__(self):
self._fetch_all()
return len(self._result_cache)
def _clone(self):
clone = self.__class__(raw_query=self.raw_query, model=self.model, using=self._db, hints=self._hints,
query=self.query, params=self.params, translations=self.translations)
return clone