给定 NDB 游标,获取上一页结果的正确方法是啥?

Posted

技术标签:

【中文标题】给定 NDB 游标,获取上一页结果的正确方法是啥?【英文标题】:What is the correct way to get the previous page of results given an NDB cursor?给定 NDB 游标,获取上一页结果的正确方法是什么? 【发布时间】:2014-02-04 13:47:15 【问题描述】:

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

如果我将代码的“获取上一页”部分更改为以下代码 sn-p,我会得到预期的输出,但是使用前向排序查询和 end_cursor 是否有任何我没有预见到的缺点而不是文档中描述的机制?

# Fetch the previous page.
bars3, cursor3, more3 = q_forward.fetch_page(2, end_cursor=cursor1)
pprint.pprint(bars3)

【问题讨论】:

***.com/questions/14543008/… --> 同样的问题 谢谢@zho。在发布问题之前,我阅读了所有与使用 ndb 游标进行反向分页相关的问题,包括您的问题。它们是类似的问题,但我认为我的示例与您的问题没有相同的问题(反转已经反转的光标)。我还尝试将我的示例归结为可以在交互式控制台中运行的内容,并将 Web 框架排除在外。 看起来像 end_cursor 的解决方案将始终只显示第一页。尝试从第三页获取第二页。 (对我不起作用)。 【参考方案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's left:
# bars1 = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

因此,文档中的示例以两个不同的顺序从两个不同的方向获取同一页面。这不是你想要达到的。

您似乎已经找到了一个很好地涵盖您的用例的解决方案,但让我建议另一个:

只需重用 cursor1 即可返回 page2。 如果我们在谈论前端并且当前页面是 page3,这意味着将 cursor3 分配给“下一个”按钮,将 cursor1 分配给“上一个”按钮。

这样您既不需要反转查询也不需要反转光标。

【讨论】:

这很有帮助,但我希望有一个使用单个光标的解决方案,这样我就不需要跟踪 Web 请求中的任何其他状态。我认为如果用户多次前进,我需要保留一堆“后退”光标,以便能够让他们多次返回 - 我理解正确吗? 当您进行反向查询时,这将返回一个新游标(在本例中为 cursor1)。此光标指向bars1 中0 之后的点。然后,您可以使用此光标返回另一页。【参考方案2】:

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

【讨论】:

谢谢。我必须说,ndb 中游标的实现是相当不称职的。您必须使用抵消和反转结果的事实......有些人特别擅长让简单的事情最终变得混乱。感谢您的帮助。

以上是关于给定 NDB 游标,获取上一页结果的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

是啥导致了这个“无效的游标状态”错误?

视图、游标是啥?

如何从数据库中获取 Graphql 中的分页游标?

SQL Server中的游标是啥意思?

游标的作用是啥?

MySQL 存储过程,获取使用游标查询的结果集