Подтвердить что ты не робот

Каков правильный способ получить предыдущую страницу результатов с помощью курсора 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)
4b9b3361

Ответ 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%, в процессе производства (из-за возможной согласованности) размещение и запрос сразу после этого скорее всего потерпят неудачу.