在给定NDB游标的情况下获取上一页结果的正确方法是什么?

时间:2014-01-15 21:20:47

标签: python google-app-engine google-cloud-datastore app-engine-ndb

我正在努力通过GAE提供API,允许用户通过一组实体向前和向后翻页。我查看了section about cursors on the NDB Queries documentation page,其中包含一些示例代码,介绍了如何通过查询结果向后翻页,但它似乎没有按预期工作。我正在使用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)

(仅供参考,您可以在本地应用引擎的交互式控制台中运行上述内容。)

以上代码打印出以下结果;请注意,结果的第三页只是第二页反转,而不是回到第一页:

[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)

2 个答案:

答案 0 :(得分:8)

为了让文档中的示例更清晰一点,让我们暂时忘记数据存储区并改为使用列表:

# 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即可返回第2页 如果我们正在讨论前端并且当前页面是第3页,这意味着将cursor3分配给'next'按钮并将cursor1分配给'previous'按钮。

这样你就不得反转查询和光标。

答案 1 :(得分:5)

我冒昧地将Bar模型更改为Character模型。该示例看起来更像Pythonic IMO; - )

我写了一个快速单元测试来演示分页,准备复制粘贴:

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%,在生产中(因为最终的一致性),放置和查询后很可能会失败。