Каков правильный способ получить предыдущую страницу результатов с помощью курсора NDB?
Я работаю над предоставлением API через GAE, который позволит пользователям переходить вперед и назад через набор объектов. Я просмотрел раздел о курсорах на странице документации по документам NDB, который включает в себя пример кода, который описывает, как страницы назад через результаты запроса, но он, похоже, не работает по своему желанию. Я использую GAE Development SDK 1.8.8.
Здесь приведена модифицированная версия этого примера, которая создает 5 образцов объектов, получает и печатает первую страницу, выполняет шаги вперед и распечатывает вторую страницу и пытается сделать шаг назад и снова распечатать первую страницу:
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)
(FYI, вы можете запустить выше в интерактивной консоли вашего локального движка.)
Приведенный выше код печатает следующие результаты; обратите внимание, что третья страница результатов - это только вторая обратная страница, вместо возврата на первую страницу:
[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')]
Я ожидал увидеть такие результаты:
[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')]
Если я изменил раздел "Получить предыдущую страницу" кода на следующий фрагмент кода, я получаю ожидаемый вывод, но есть ли какие-то недостатки, которые я не видел, чтобы использовать запрос с прямым заказом и end_cursor вместо механизм, описанный в документации?
# Fetch the previous page.
bars3, cursor3, more3 = q_forward.fetch_page(2, end_cursor=cursor1)
pprint.pprint(bars3)
Ответы
Ответ 1
Чтобы сделать пример из документов немного понятнее, забудьте о хранилище данных на мгновение и работайте со списком:
# 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 left:
# bars1 = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Таким образом, пример из документов извлекает одну и ту же страницу из двух разных направлений в двух разных порядках. Это не то, чего вы хотите достичь.
Кажется, вы уже нашли решение, которое хорошо охватывает ваш прецедент, но позвольте мне предложить другое:
Просто повторите курсор1, чтобы вернуться на страницу2.
Если мы говорим о внешнем интерфейсе, а текущая страница - стр. 3, это означало бы назначение курсора3 кнопке "next" и курсору 1 на "предыдущую" кнопку.
Таким образом, вы не должны отменить ни запрос, ни курсор (ы).
Ответ 2
Я взял на себя смелость изменить модель Bar
на модель Character
. Пример выглядит больше Pythonic IMO; -)
Я написал быстрый unit test, чтобы продемонстрировать разбивку на страницы, готовые для копирования:
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)
Некоторое объяснение:
Метод setUp
сначала инициализирует необходимые заглушки. Затем 6 примерных символов помещаются с идентификатором, поэтому порядок не случайный. Поскольку есть 6 символов, у нас есть 3 страницы из 2 символов. Вторая страница выбирается напрямую, используя упорядоченный запрос и смещение 2. Обратите внимание на смещение, это ключ для примера.
test_current_page
проверяет наличие двух средних символов. Символы сравниваются по имени для удобочитаемости.; -)
test_next_page
выбирает следующую (третью) страницу и проверяет имена ожидаемых символов. До сих пор все довольно прямолинейно.
Теперь интересен test_previous_page
. Это делает пару вещей, сначала курсор меняет направление, поэтому курсор теперь указывает назад, а не вперед. (Это улучшает читаемость, оно должно работать без этого, но смещение будет другим, я оставлю это как упражнение для читателя.) Затем запрос создается с обратным порядком, это необходимо, потому что смещение не может быть отрицательным и вы хотите иметь предыдущие объекты. Затем результаты извлекаются со смещением, равным длине страницы текущей страницы. Иначе запрос вернет те же результаты, но отменит (как в вопросе). Теперь, поскольку запрос был обратным порядком, результаты все назад. Мы просто отменим список результатов на месте, чтобы исправить это. И последнее, но не менее важное: ожидаемые имена утверждаются.
Боковое примечание. Поскольку это связано с глобальными запросами, вероятность устанавливается равной 100%, в процессе производства (из-за возможной согласованности) размещение и запрос сразу после этого скорее всего потерпят неудачу.