La clase que hay debajo de paginate_by, el aviso que casi nadie ve, y por qué la página mil tarda más que la primera.
⏱ 14 min de lectura
La lección 23 puso paginate_by = 2 en un ListView y salió una lista paginada. Esta lección abre esa caja, porque hay tres cosas que conviene saber y que allí quedaron apuntadas de pasada: cómo se pagina una vista de función, qué aviso salta cuando la consulta no está ordenada, y por qué este mecanismo se vuelve lento con muchas filas.
start_index y end_index son los que permiten escribir "mostrando del 4 al 6 de 13", que se agradece más de lo que parece. Y la página se recorre directamente con un {% for %}, sin tener que entrar a object_list.
from django.core.paginator import Paginator
deflista(peticion):
contactos = Contacto.objects.select_related('grupo').order_by('nombre')
paginador = Paginator(contactos,20)
pagina = paginador.get_page(peticion.GET.get('page'))return render(peticion,'contactos/lista.html',{'pagina': pagina})
Cuatro líneas. En la plantilla, el mismo trozo de la lección 23, con el nombre que hayas elegido en lugar de page_obj:
html
{% for contacto in pagina %}<li>{{ contacto.nombre }}</li>{% endfor %}
Y ahí está la conexión con la lección 23: eso es literalmente lo que hace ListView por dentro, y page_obj es el nombre con el que lo deja en el contexto.
Los dos sacan una página y se comportan de forma muy distinta con lo que llega de la dirección:
Salida en pantalla
page(99) -> EmptyPage: Esa página no contiene resultados
page(0) -> EmptyPage: Este número de página es menor que 1
page('abc') -> PageNotAnInteger: Este número de página no es un entero
page(None) -> PageNotAnInteger: Este número de página no es un entero
page lanza y get_pagenunca falla. Como lo que llega en ?page= es texto que escribe cualquiera, get_page es lo que se usa en las vistas, y por eso la vista de arriba no tiene ni un try.
Hay una rareza ahí que conviene mirar dos veces: get_page(0) devuelve la última página, no la primera. El motivo es que 0 produce un EmptyPage, y ante un EmptyPage la función devuelve la última; los valores que no son números producen PageNotAnInteger y devuelven la primera. Es coherente por dentro y sorprende por fuera.
Cuando quieras el comportamiento de ListView, que es devolver 404, hay que usar page y capturar:
python
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.http import Http404
try:
pagina = paginador.page(peticion.GET.get('page'))except PageNotAnInteger:
pagina = paginador.page(1)except EmptyPage:raise Http404('Esa página no existe')
Las dos posturas son defendibles. Devolver la primera página es más amable; devolver 404 evita que los buscadores indexen mil direcciones con el mismo contenido.
solo count: 1 consulta -> SELECT COUNT(*) FROM "contactos_contacto"
la página: 1 consulta -> SELECT ... ORDER BY "nombre" ASC LIMIT 3 OFFSET 3
Dos consultas siempre, como se midió en la lección 23. La primera es el recuento, que hace falta para saber cuántas páginas hay, y la segunda son los datos.
Lo interesante es el OFFSET, porque explica un comportamiento que desconcierta cuando la tabla crece: una base de datos no sabe saltarse filas sin leerlas. Para OFFSET 20000 lee veinte mil filas, las descarta y devuelve las veinte siguientes.
O sea que la página mil tarda mil veces más que la primera. Con tablas pequeñas da igual; con un millón de filas, las últimas páginas son inutilizables.
Y el COUNT(*) tiene el mismo problema por su cuenta: contar un millón de filas no es gratis, y se hace en cada carga de cada página.