【问题标题】:Trying to understand how a Django foreign key works试图了解 Django 外键是如何工作的
【发布时间】:2017-09-03 18:36:29
【问题描述】:

所以我现在正在构建我的第一个 Django 项目,一个双人国际象棋网站。我快完成了,现在我正在编写测试。该项目有两个原始模型:玩家和游戏。 Player 模型与 User 模型具有一对一的字段 Player.user 和 Game 的外键 Player.current_game,它指示玩家当前所在的游戏实例。 Game.draw_offered CharField 采用值 "" (默认值)、“w”或“b”,指示是否提供了平局以及提供平局的玩家的颜色。我有一个执行以下操作的测试用例:

class Draw_ViewTestCase(TestCase):

    def setUp(self):
        self.user1 = User.objects.create_user(username="Test_User_1", password="test1")
        self.user2 = User.objects.create_user(username="Test_User_2", password="test2")
        self.factory = RequestFactory()
        self.game = Game.objects.create()
        self.player1 = Player.objects.create(user=self.user1, current_game=self.game, color="w")
        self.player2 = Player.objects.create(user=self.user2, current_game=self.game, color="b")

    def test_draw(self): 
        request = self.factory.get(reverse('draw'))
        request.user = self.user1
        #The draw view changes the draw_offered field of the player's current game to the player's color, and saves the current_game to the database
        response = draw(request)
        self.game.refresh_from_db()
        assert self.game.draw_offered == self.player1.color
        assert self.game == self.player2.current_game
        #self.player2.current_game.refresh_from_db()
        assert self.game.draw_offered == self.player2.current_game.draw_offered

除了最后一个之外,所有的断言都通过了,但是如果你取消对倒数第二行的注释,它就会通过。

发生了什么事?据我了解,当您引用外键属性 self.player2.current_game 时,Django 会执行数据库查找并返回带有最新字段的 Game 实例。由于 self.game 和 self.player2.current_game 对应数据库中相同的 Game 记录,而 self.game.refresh_from_db() 只是被调用,你会认为 self.game.draw_offered == self.player2.current_game.draw_offered将评估为 True——两个 Game 实例都引用相同的数据库记录并具有最新的字段。我必须调用 self.player2.current_game.refresh_from_db() 以使断言通过这一事实对我来说没有意义——根据我对 Django Foreign Keys 的理解,self.player2.current_game 应该自动更新与数据库。

【问题讨论】:

    标签: python sql django generic-foreign-key


    【解决方案1】:

    这与外键无关。

    您缺少的是,仅仅因为两个实例指向同一个数据库记录,并不意味着它们是同一个对象。正如您必须刷新 self.game 才能在 draw 视图中查看对基础记录所做的更改一样,您还需要刷新 self.player2.current_game 以便它也获得这些更新的值。

    如果 Django 认为它已经拥有外键,它不会进行数据库调用来查找外键:确实如此,因为您最初通过传入整个 Game 对象创建了 self.player1self.player2。所以是的,您需要显式刷新 current_game 对象以查看所做的更改。

    如果你真的想要,你可以不这样做,通过传递 ID 而不是完整的对象来创建对象:self.player2 = Player.objects.create(...current_game_id=self.game.id...)。这样,Django 实际上不会缓存对象,因此需要在测试结束时查询它。

    【讨论】:

    • 这是我的理解。当您编写 x = self.player2.current_game 时,x 的字段会自动与数据库保持同步,因为每当您通过外键属性引用对象时,Django 都会执行数据库查找。您的回答似乎暗示 self.player2.current_game 更像是一个标准的 Python 属性引用,它指向一个存储在内存中的对象,与数据库无关。哪一个是正确的?
    • 不,正如我在第三段开头所说的那样,您的理解是错误的。当你最初引用一个对象的外键属性时,Django 会去数据库获取它。但是,它将其存储在父对象中,因此后续引用不会导致进一步的数据库查找。 (同样,如果您最初使用select_related,Django 将从头开始执行 JOIN 来获取和缓存相关对象,因此不会进行数据库查找。)在您的情况下,再次如我所述,Django 的对象来自当您创建它时,无需进行数据库查找。
    • 我有很多视图接受请求,设置 {player = request.user.player},然后设置 {game = player.current_game},它们的行为取决于 {game} 的属性。每次我这样做时是否需要重写这些视图以包含 refresh_from_db() 语句以确保 {game} 的属性是最新的,或者在某些情况下引用外键可以保证数据库查找将被执行?因为我的项目似乎运行良好,如果 {game} 的属性过时,我预计会有一些问题。谢谢你的帮助,顺便说一句。
    • 不,因为只有在测试中你才能在本地创建一个对象,调用一个视图来进行自己的获取和处理,然后断言原始本地对象的值。在其他任何地方,您都可以根据需要获取对象并传递它们。
    【解决方案2】:

    测试运行程序将在每次测试之前运行setUp 方法,因此您在运行代码之前就在实例上运行。 Unitest 文档中对此进行了很好的解释。

    您没有从数据库访问新的player2 实例,只是加载它,正如setUp 方法中定义的那样。

    【讨论】:

    • 是的,但这有什么关系呢?无论 player2 是否刷新,它的 current_game 外键都指向数据库中的同一条记录,调用 player2.current_game 应该返回一个与该记录具有相同最新字段值的 Game 实例。 self.game 对应的是同一条记录,刚刚从数据库中刷新,那么 self.game 和 player2.current_game 的字段值不应该相同吗?基于这种行为 player.current_game 更像一个游戏实例,其字段值存储在内存中,而不是直接从数据库中读取。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-13
    • 2021-07-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-30
    相关资源
    最近更新 更多