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

10
demandé sur ChillarAnand 2010-03-28 13:54:59

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

8
répondu Tomasz Zielinski 2010-03-28 10:31:37

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.

2
répondu Felix Kling 2010-03-28 10:06:34

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)
2
répondu Jonathan Potter 2017-02-27 17:12:17

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
1
répondu TGO 2017-05-23 12:17:54