Search code examples
pythonsqldjangogeneric-foreign-key

Trying to understand how a Django foreign key works


So I'm building my first Django project now, a two-player chess website. I'm almost done and now I'm writing the tests. The project has two original models: Player and Game. The Player model has a one to one field to the User model, Player.user, and a foreign key to Game, Player.current_game, which indicates the game instance the player is currently in. The Game.draw_offered CharField takes the values "" (the default value), "w", or "b", indicating whether a draw has been offered and the color of the player who offered it. I have a test case that does the following:

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

All the assertions pass but the last one, but if you uncomment the second to last line, it passes.

What's going on? As I understand it, when you refer to the Foreign Key attribute self.player2.current_game, Django performs a database lookup and returns a Game instance with up-to-date fields. Since self.game and self.player2.current_game correspond to the same Game record in the database, and self.game.refresh_from_db() was just called, you would think that self.game.draw_offered == self.player2.current_game.draw_offered would evaluate to True-- both Game instances refer to the same database record and have up-to-date fields. The fact that I have to call self.player2.current_game.refresh_from_db() to make the assertion pass doesn't make sense to me-- according to my understanding of Django Foreign Keys, self.player2.current_game should automatically be up to date with the database.


Solution

  • This isn't really about foreign keys.

    What you're missing is that just because two instances point to the same database record, it doesn't mean that they're the same object. Just as you have to refresh self.game to see the changes that were made to the underlying record in the draw view, you will also need to refresh self.player2.current_game so that it also gets those updated values.

    Django won't make a database call to lookup a foreign key if it thinks it already has it: and it does, because you originally created self.player1 and self.player2 by passing in the entire Game object. So yes, you need to explicitly refresh the current_game object to see the changes that were made.

    If you really wanted, you could probably create the objects without doing this, by passing the ID rather than the full object: self.player2 = Player.objects.create(...current_game_id=self.game.id...). This way, Django won't actually have the object cached, so it will need to query it at the end of your test.