python - how - ndb put
¿Cuál es la forma correcta de obtener la página anterior de resultados con un cursor NDB? (2)
Estoy trabajando para proporcionar una API a través de GAE que permita a los usuarios avanzar y retroceder páginas a través de un conjunto de entidades. Revisé la sección sobre cursores en la página de documentación Consultas de NDB , que incluye algunos ejemplos de código que describe cómo retroceder página a través de los resultados de la consulta, pero parece que no funciona como se desea. Estoy usando GAE Development SDK 1.8.8.
Aquí hay una versión modificada de ese ejemplo que crea 5 entidades de muestra, obtiene e imprime la primera página, avanza e imprime la segunda página, e intenta retroceder e imprimir la primera página nuevamente:
import pprint
from google.appengine.ext import ndb
class Bar(ndb.Model):
foo = ndb.StringProperty()
#ndb.put_multi([Bar(foo="a"), Bar(foo="b"), Bar(foo="c"), Bar(foo="d"), Bar(foo="e")])
# Set up.
q = Bar.query()
q_forward = q.order(Bar.foo)
q_reverse = q.order(-Bar.foo)
# Fetch the first page.
bars1, cursor1, more1 = q_forward.fetch_page(2)
pprint.pprint(bars1)
# Fetch the next (2nd) page.
bars2, cursor2, more2 = q_forward.fetch_page(2, start_cursor=cursor1)
pprint.pprint(bars2)
# Fetch the previous page.
rev_cursor2 = cursor2.reversed()
bars3, cursor3, more3 = q_reverse.fetch_page(2, start_cursor=rev_cursor2)
pprint.pprint(bars3)
(Para su información, puede ejecutar lo anterior en la consola interactiva de su motor de aplicación local).
El código anterior imprime los siguientes resultados; tenga en cuenta que la tercera página de resultados es solo la segunda página invertida, en lugar de volver a la primera página:
[Bar(key=Key(''Bar'', 4996180836614144), foo=u''a''),
Bar(key=Key(''Bar'', 6122080743456768), foo=u''b'')]
[Bar(key=Key(''Bar'', 5559130790035456), foo=u''c''),
Bar(key=Key(''Bar'', 6685030696878080), foo=u''d'')]
[Bar(key=Key(''Bar'', 6685030696878080), foo=u''d''),
Bar(key=Key(''Bar'', 5559130790035456), foo=u''c'')]
Esperaba ver resultados como este:
[Bar(key=Key(''Bar'', 4996180836614144), foo=u''a''),
Bar(key=Key(''Bar'', 6122080743456768), foo=u''b'')]
[Bar(key=Key(''Bar'', 5559130790035456), foo=u''c''),
Bar(key=Key(''Bar'', 6685030696878080), foo=u''d'')]
[Bar(key=Key(''Bar'', 6685030696878080), foo=u''a''),
Bar(key=Key(''Bar'', 5559130790035456), foo=u''b'')]
Si cambio la sección "Obtener la página anterior" del código al siguiente fragmento de código, obtengo el resultado esperado, pero ¿hay algún inconveniente que no haya previsto al usar la consulta ordenada por adelantado y end_cursor en lugar del mecanismo descrito? en la documentación?
# Fetch the previous page.
bars3, cursor3, more3 = q_forward.fetch_page(2, end_cursor=cursor1)
pprint.pprint(bars3)
Para que el ejemplo de los documentos sea un poco más claro, olvidemos por un momento el almacén de datos y trabaje con una lista:
# some_list = [4, 6, 1, 12, 15, 0, 3, 7, 10, 11, 8, 2, 9, 14, 5, 13]
# Set up.
q = Bar.query()
q_forward = q.order(Bar.key)
# This puts the elements of our list into the following order:
# ordered_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
q_reverse = q.order(-Bar.key)
# Now we reversed the order for backwards paging:
# reversed_list = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
# Fetch a page going forward.
bars, cursor, more = q_forward.fetch_page(10)
# This fetches the first 10 elements from ordered_list(!)
# and yields the following:
# bars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# cursor = [... 9, CURSOR-> 10 ...]
# more = True
# Please notice the right-facing cursor.
# Fetch the same page going backward.
rev_cursor = cursor.reversed()
# Now the cursor is facing to the left:
# rev_cursor = [... 9, <-CURSOR 10 ...]
bars1, cursor1, more1 = q_reverse.fetch_page(10, start_cursor=rev_cursor)
# This uses reversed_list(!), starts at rev_cursor and fetches
# the first ten elements to it''s left:
# bars1 = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Entonces, el ejemplo de los documentos obtiene la misma página de dos direcciones diferentes en dos órdenes diferentes. Esto no es lo que quieres lograr.
Parece que ya encontraste una solución que cubre tu caso de uso bastante bien, pero déjame sugerirte otra:
Simplemente vuelva a usar cursor1 para volver a la página2.
Si hablamos frontend y la página actual es página3, esto significaría asignar cursor3 al botón ''next'' y cursor1 al botón ''previous''.
De esta forma, no debe invertir ni la consulta ni el cursor (s).
Me tomé la libertad de cambiar el modelo de Bar
a un modelo de Character
. El ejemplo parece más Pythonic IMO ;-)
Escribí una prueba unitaria rápida para demostrar la paginación, lista para copiar y pegar:
import unittest
from google.appengine.datastore import datastore_stub_util
from google.appengine.ext import ndb
from google.appengine.ext import testbed
class Character(ndb.Model):
name = ndb.StringProperty()
class PaginationTest(unittest.TestCase):
def setUp(self):
tb = testbed.Testbed()
tb.activate()
self.addCleanup(tb.deactivate)
tb.init_memcache_stub()
policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy(
probability=1)
tb.init_datastore_v3_stub(consistency_policy=policy)
characters = [
Character(id=1, name=''Luigi Vercotti''),
Character(id=2, name=''Arthur Nudge''),
Character(id=3, name=''Harry Bagot''),
Character(id=4, name=''Eric Praline''),
Character(id=5, name=''Ron Obvious''),
Character(id=6, name=''Arthur Wensleydale'')]
ndb.put_multi(characters)
query = Character.query().order(Character.key)
# Fetch second page
self.page = query.fetch_page(2, offset=2)
def test_current_page(self):
characters, _cursor, more = self.page
self.assertSequenceEqual(
[''Harry Bagot'', ''Eric Praline''],
[character.name for character in characters])
self.assertTrue(more)
def test_next_page(self):
_characters, cursor, _more = self.page
query = Character.query().order(Character.key)
characters, cursor, more = query.fetch_page(2, start_cursor=cursor)
self.assertSequenceEqual(
[''Ron Obvious'', ''Arthur Wensleydale''],
[character.name for character in characters])
self.assertFalse(more)
def test_previous_page(self):
_characters, cursor, _more = self.page
# Reverse the cursor (point it backwards).
cursor = cursor.reversed()
# Also reverse the query order.
query = Character.query().order(-Character.key)
# Fetch with an offset equal to the previous page size.
characters, cursor, more = query.fetch_page(
2, start_cursor=cursor, offset=2)
# Reverse the results (undo the query reverse ordering).
characters.reverse()
self.assertSequenceEqual(
[''Luigi Vercotti'', ''Arthur Nudge''],
[character.name for character in characters])
self.assertFalse(more)
Alguna explicación:
El método setUp
primero inicializa los stubs necesarios. Luego, los 6 personajes de ejemplo se ponen con una identificación para que el orden no sea aleatorio. Como hay 6 caracteres, tenemos 3 páginas de 2 caracteres. La segunda página se obtiene directamente usando una consulta ordenada y un desplazamiento de 2. Observe el desplazamiento, esta es la clave para el ejemplo.
test_current_page
verifica que se hayan obtenido los dos caracteres intermedios. Los personajes se comparan por su nombre para la legibilidad. ;-)
test_next_page
busca la siguiente (tercera) página y verifica los nombres de los caracteres esperados. Todo es bastante sencillo hasta ahora.
Ahora test_previous_page
es interesante. Esto hace un par de cosas, primero el cursor se invierte para que el cursor apunte hacia atrás en lugar de hacia adelante. (Esto mejora la legibilidad, debería funcionar sin esto, pero el desplazamiento será diferente, lo dejaré como un ejercicio para el lector.) A continuación, se crea una consulta con un orden inverso, esto es necesario porque el desplazamiento no puede ser negativo y quieres tener entidades previas. A continuación, los resultados se obtienen con un desplazamiento igual a la longitud de página de la página actual. De lo contrario, la consulta arrojará los mismos resultados, pero se invertirá (como en la pregunta). Ahora, debido a que la consulta se ordenó de forma inversa, los resultados son todos al revés. Simplemente revertimos la lista de resultados en el lugar para arreglar esto. Por último, pero no menos importante, se afirman los nombres esperados.
Nota al margen: dado que esto involucra consultas globales, la probabilidad se establece en 100%, en producción (debido a la consistencia eventual) la puesta en práctica y las consultas posteriores probablemente fracasen.