Django: ORM player substitution, checking __isnull=False versus is not None
I have a Django model Player. Now this model has a save
method which goes like this:
def save(self, *args, **kwargs):
"""
Overrides the save method to convert the 'name' attribute to uppercase before saving.
"""
player = Player.objects.filter(
Q(fide_id=self.fide_id, fide_id__isnull=False)
| Q(lichess_username=self.lichess_username, lichess_username__isnull=False)
| Q(
name=self.name,
email=self.email,
email__isnull=False,
name__isnull=False,
)
)
if player.exists():
p = player.first()
for k, v in self.__dict__.items():
if k == "_state":
continue
if v is None:
self.__dict__[k] = p.__dict__[k]
if self.check_lichess_user_exists():
self.get_lichess_user_ratings()
print(f"id: {self.id}")
super().save(*args, **kwargs)
This is intended to look for a player which has either:
- The same fide_id (which should not be null, as then I would be considering two players with null values in their fide id to be the same)
- The same lichess_username (idem)
- The same combination of username and email (idem)
Then, if such a player exists, then set every field of the player we're adding (if it is None) to the fields of the player already in the database. Then super().save()
should overwrite the player with the same id, according to the Django docs:
You may have noticed Django database objects use the same save() method for creating and changing objects. Django abstracts the need to use INSERT or UPDATE SQL statements. Specifically, when you call save() and the object’s primary key attribute does not define a default or db_default, Django follows this algorithm: If the object’s primary key attribute is set to anything except None, Django executes an UPDATE.
If the object’s primary key attribute is not set or if the UPDATE didn’t update anything (e.g. if primary key is set to a value that doesn’t exist in the database), Django executes an INSERT.
If the object’s primary key attribute defines a default or db_default then Django executes an UPDATE if it is an existing model instance and primary key is set to a value that exists in the database. Otherwise, Django executes an INSERT.
The one gotcha here is that you should be careful not to specify a primary-key value explicitly when saving new objects, if you cannot guarantee the primary-key value is unused. For more on this nuance, see Explicitly specifying auto-primary-key values above and Forcing an INSERT or UPDATE below.
When I run this with some tests, however, I get some problems.
Here's my test setup:
def setUp(self):
# create Tournament
tournament_name = 'tournament_01'
tournament = Tournament.objects.create(
name=tournament_name,
tournament_type=TournamentType.DOUBLEROUNDROBIN)
# create round
round_name = 'round_01'
self.round = Round.objects.create(
name=round_name, tournament=tournament)
# create two players
self.players = []
player = Player.objects.create(
lichess_username='alpega')
tournament.players.add(player)
self.players.append(player)
player = Player.objects.create(
lichess_username='fernanfer')
tournament.players.add(player)
self.players.append(player)
tournament.save()
And I get the following errors (multiple times) on the test:
Traceback (most recent call last):
File "test_models_game.py", line 27, in setUp
player = Player.objects.create(
^^^^^^^^^^^^^^^^^^^^^^
(... SQL cursor errors)
django.db.utils.IntegrityError: UNIQUE constraint failed: chess_models_player.id
Line 27 is player = Player.objects.create(lichess_username='fernanfer')
However, if I substitute the query for the more intuitive approach:
query = Q()
if self.fide_id:
query |= Q(fide_id=self.fide_id)
if self.lichess_username:
query |= Q(lichess_username=self.lichess_username)
if self.name and self.email:
query |= Q(name=self.name) & Q(email=self.email)
player = Player.objects.filter(query)
Then it works just fine. I then have two questions.
- Why does the first query not work and the second one does? The logic behind it seems correct.
- Why is Django not doing an SQL UPDATE on the player with the id which is supposed to be the primary key instead of trying to insert a new one?
I have found this: https://docs.djangoproject.com/en/2.2/ref/models/fields/#primary-key
The primary key field is read-only. If you change the value of the primary key on an existing object and then save it, a new object will be created alongside the old one.
But I am not sure it applies here