如何通过NDB游标获取上一页的结果?

15

我正在开发一个通过GAE提供API的项目,该API将允许用户在一组实体中向前和向后翻页。 我已经查阅了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
http://stackoverflow.com/questions/14543008/python-backward-paging?rq=1 --> 我提出的同样的问题 - zho
1
感谢@zho。在发布问题之前,我阅读了与使用ndb游标进行反向分页相关的每个问题,包括您的问题。这些都是类似的问题,但我认为我的例子没有您的问题那么严重(即翻转已经反向的游标)。我还尝试将我的示例简化为可在交互式控制台中运行并将Web框架排除在外的内容。 - Greg
看起来使用 end_cursor 的解决方案总是只显示第一页。尝试从第三页获取第二页。(对我来说不起作用)。 - Dmytro Sadovnychyi
2个回答

9

为了让文档中的示例更加清晰,让我们暂时忘记数据存储器,使用列表来代替:

# 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]

所以文档中的示例从两个不同的方向以两种不同的顺序获取相同的页面。这不是您想要实现的目标。
看起来您已经找到了一个非常好地覆盖您用例的解决方案,但让我建议另一个:
简单地重复使用cursor1返回到page2。
如果我们在谈论前端并且当前页面是page3,则意味着将cursor3分配给“下一页”按钮,将cursor1分配给“上一页”按钮。
这样,您既不需要颠倒查询也不需要颠倒光标。

2
这很有帮助,但我希望有一个适用于单个光标的解决方案,这样我就不需要在 Web 请求之间跟踪任何其他状态。如果用户多次前进,我认为我需要保持“后退”光标的堆栈,以便能够让他们多次返回 - 我理解得对吗? - Greg
当您执行反向查询时,它将返回一个新的光标(在本例中为cursor1)。该光标指向bars1中0点后的位置。然后您可以使用该光标向后翻页。 - Patrick Costello

6
我已经更改了 Bar 模型为 Character 模型,这个例子看起来更像是 Python 代码。;-)
我编写了一个快速单元测试来演示分页功能,可以直接复制粘贴使用:
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个示例字符与一个ID放在一起,以使顺序不是随机的。由于有6个字符,因此我们有3页2个字符。使用有序查询和偏移量2直接获取第二页。请注意偏移量,这是本例的关键。

test_current_page验证获取了中间的两个字符。为了易读性,字符按名称进行比较。

test_next_page获取下一页(第三页)并验证期望字符的名称。到目前为止,一切都很简单。

现在test_previous_page很有趣。这做了几件事情,首先,游标被反转,因此游标现在指向后面而不是前面。 (这提高了可读性,应该可以在没有这个的情况下工作,但偏移量将不同,我将把这留给读者作为练习。)接下来创建一个带有反向排序的查询,这是必要的,因为偏移量不能为负,并且您想要有以前的实体。然后使用当前页面的页长度作为偏移量获取结果。否则,查询将返回相同的结果,但是反向(如问题所述)。现在,由于查询是反向排序的,因此结果全部反向。我们只需在原地翻转结果列表即可修复此问题。最后但并非最不重要的是,断言期望的名称。

附注:由于这涉及全局查询,因此概率设为100%,在生产中(由于最终一致性),紧接着进行放置和查询很可能会失败。


谢谢。我必须说,ndb中游标的实现相当不称职。你不得不使用偏移量并反转结果...有些人擅长把简单的事情搞得一团糟。感谢您的帮助。 - Luca

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接