2010-03-28 23 views
9

Estoy usando Django Paginator en todas partes en mi sitio web e incluso escribí una etiqueta de plantilla especial, para que sea más conveniente. Pero ahora llegué a un estado en el que necesito hacer una consulta compleja de SQL sin formato, que sin un LIMIT devolverá unos 100K registros.Django: Paginator + consulta SQL sin procesar

¿Cómo puedo utilizar Django Pagintor con una consulta personalizada?

ejemplo simplificado de mi problema:

Mi modelo:

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 forma en que uso pagintation con 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 esta manera, Django se ponen muy inteligente, y agrega LIMIT a una consulta al ejecutarlo. Pero cuando se utiliza gestor de encargo:

all_objects = Person.objects.complicated_list(); 

se selecciona todos los datos, y sólo entonces se corta lista de Python, que es muy lento. ¿Cómo puedo hacer que mi administrador personalizado se comporte de forma similar al construido en uno?

+0

En Python, no debe usar espacios en blanco cuando lo desee en su clase Person. – Gereltod

Respuesta

8

En cuanto al código fuente de Paginator, page() function en particular, creo que solo es cuestión de implementar slicing en su lado, y traducir eso a la cláusula LIMIT relevante en la consulta SQL. También podría ser necesario añadir un poco de almacenamiento en caché, pero esto empieza a parecerse a QuerySet, por lo que tal vez se puede hacer otra cosa:

  • puede crear VISTA base de datos utilizando CREATE VIEW myview AS [consulta];
  • añadir modelo de Django para ese punto de vista, la Meta: managed=False
  • uso de ese modelo como cualquier otro modelo, incluyendo cortar sus QuerySets - esto significa que es perfectamente adecuado para el uso con Paginator

(Para su información - I' he estado usando este método desde hace mucho tiempo, incluso con complejas relaciones muchos-a-muchos con vistas falsificar cuadros intermedios M2M.)

+0

Guau, eso es genial :) Gracias por la respuesta, pero creo que estoy buscando direcciones equivocadas. He hecho otra pregunta: http://stackoverflow.com/questions/2532686/django-getting-the-list-of-related-records-for-a-list-of-objects Los gerentes personalizados de Guiess no son el Lo mejor aquí. –

2

No sé sobre Django 1.1 pero si se puede esperar a 1.2 (que no debe ser más larga que) se puede hacer uso de objects.raw() como se describe en this article y en el development documentation.

De lo contrario, si la consulta no es demasiado compleja, tal vez con el extra clause es suficiente.

+0

Gracias por un consejo útil. Pero supongo que no ayuda en mi situación –

+0

Aún no puede obtener el conteo de los resultados de una consulta sin formato. Parece que realmente tiene que hacer una lista (objects.raw()) para que funcione con paginator. Gracias a http://stackoverflow.com/questions/2317452/django-count-rawqueryset por esa información. – Josh

1

Aquí está una clase RawPaginator hice que anula Paginator para trabajar con consultas primas. Se necesita un argumento adicional, count, que es el recuento total de su consulta. No corta el object_list porque debe paginar en su consulta sin procesar a través de OFFSET y 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) 
+0

Muy buena solución. Gracias. –

+0

Cuando llama a la nueva clase Paginator, ¿qué valor 'contendrá' cuando se da como argumento? –

0

También quería tapar un PaginatedRawQuerySet que escribí (por favor considere esto como una versión alfa). Esto agrega la capacidad de corte a un conjunto de consulta sin formato. Consulte to this answer, que escribí para otra pregunta con un requisito similar, para comprender cómo funciona (especialmente la sección "Una palabra de advertencia" al final).

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