Python: Change in Django model not recognised

Question:

This must be really simple, but I can’t get my head around it.
I want to assign a simple one-character value to a django model instance. I have done that a million times, but in this case, it does notwork.

my models.py

class MetaInformation(models.Model):
    # part of the class profile ...
    PROFILE_STATUS = (
        ('C', 'Claimed'),
        ('U', 'Unclaimed'),)
    status = models.CharField(_('Profile'), max_length=1,
        choices = PROFILE_STATUS,)

class Profile(MetaInformation):
    # additional attributes
    ...

Now I am executing in the Django shell:

In [1]: a = Profile.objects.all()
In [2]: a[1].status
Out[2]: u'U'
In [3]: a[1].status = Profile.PROFILE_STATUS[0] # equal to 'C'
In [4]: a[1].save()

I would expect that the result is

In [14]: a[1].status
Out[14]: u'C'

but Django returns

In [14]: a[1].status
Out[14]: u'U'

Why is the saving of the attribute not recognised or any error message provided?

Asked By: neurix

||

Answers:

PROFILE_STATUS[0] is going to return the ('C', 'Claimed') and its a tuple. You need PROFILE_STATUS[0][0]:

a[1].status = Profile.PROFILE_STATUS[0][0]
Answered By: Aamir Rind

Have a look at this –

>>> from apps.users.models import Member
>>> members = Member.objects.all()
>>> members[1].user_type
u'C'
>>> members[1].user_type = 'M'
>>> members[1].save()
>>> members[1].user_type
u'C'
>>> m = members[1]
>>> m.user_type
u'C'
>>> m.user_type = 'M'
>>> m.save()
>>> m.user_type
'M'

here is what I think is happening: The all() method returns a QuerySet. And from the queries above, the changes are not being committed to database when you execute save() on an item in the QuerySet. But it does work if you do it on a Member object individually. And from Django documentation –

QuerySets are lazy — the act of creating a QuerySet doesn’t involve
any database activity. You can stack filters together all day long,
and Django won’t actually run the query until the QuerySet is
evaluated. …

In general, the results of a QuerySet aren’t fetched from the database
until you “ask” for them. When you do, the QuerySet is evaluated by
accessing the database.

You can read more here. So the save() method on members[1] doesn’t touch the database or read back from there. While the changes on the Member objects are committed to database and read back immediately.

Answered By: Bibhas Debnath

Bibhas has most of the right answer, I believe. The fact that you are always referencing the data through a QuerySet, Profile.objects.all() is the the largest part of it. Couple that with the way that Django handles slicing (like list indexing, but a QuerySet isn’t really a list), and the fact that databases don’t have to consider records ordered, and you have a fantastic source of confusion and frustration.

The sequence

a = Profile.objects.all()
a[1].status

will produce SQL similar to this:

SELECT * FROM user_profile LIMIT 1 OFFSET 1;

in order to get the single value from the table. There are a couple of important things here:

  1. Only one record is fetched. This is done for efficiency, since you only asked for a single record, there’s no point in retrieving any more. This also means that
  2. Django won’t populate the QuerySet’s cache. This means that the next time you ask for a[1], it will hit the database again.
  3. There is no order specified on this query. The database is free to return the rows in whatever order is most efficient for it, and that order can even change between queries.

I know that MySQL, specifically, will often return records in the order in which they were most recently updated. So just by saving the record, it becomes unlikely to be in the same position as it was before. It may move to the start of the result set, or the end of it, but it is unlikely to remain in position 1 (the second item returned).

To avoid this, don’t treat the queryset like a list. Don’t perform operations directly on items that you have retrieved using slicing syntax. If you need to do that, then store them in a temporary variable, and perform all of the operations on that variable.

This small modification would have avoided all of the trouble:

>>> a = Profile.objects.all()
>>> b = a[1]
>>> b.status
'U'
>>> b.status = 'C'
>>> b.save()
>>> b.status
'C'

Alternatively, if you need to treat it like a list, then make it one. Making a list out of a QuerySet will completely evaluate it, and will store the entire result set in memory, where a[1] is guaranteed to be the same object every time you ask for it.

>>> a = list(Profile.objects.all()) # Warning -- may be huge if the Profile table is large
>>> a[1].status
'U'
>>> a[1].status = 'C'
>>> a[1].save()
>>> a[1].status
'C'
Answered By: Ian Clelland
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.