Jerry
Jerry

Reputation: 1812

Django queryset indexing shuffle around and doesn't remember my changes

Background

I have model C inherits B

In C, it has just normal charField, nullBooleanField and foreign keys.

class C(B):
  uid = UUIDField(unique=True)
  x = models.ForeignKey(X, to_field='uid')

  y = models.ForeignKey(Y, to_field='uid', null=True)

  z = models.TextField()
  u = models.ForeignKey(U, null=True)
  foo = models.NullBooleanField(default=False)

In B, it has bunch of dateField like below

class B(models.Model):
  date_added = models.DateTimeField(auto_now_add=True)
  date_modified = models.DateTimeField(auto_now=True)
  date_deactivated = models.DateTimeField(null=True, blank=True)
  active = models.BooleanField(default=True, db_index=True, 
    help_text='Set to false if the instance is deleted.')
  class Meta:
    abstract = True

None of those classes has meta order specified.

The problem

When I was in a shell_plus, I assigned C.objects.all() to a variable called cs, then I can see cs[0], cs[1], cs[2] and cs[3]. There are only 4 records. Now I want to change one nullBooleanField to false/true. I did cs[1].foo = true and cs[1].save()

I would expect 4 records are in the same order as when I fetched them and the second record's foo field is updated.

The actual result is, for some reasons, the second record is moved to the back of the list, and it doesn't remember my changes.

For example, in the beginning, 4 records are R1, R2, R3, R4 and R2.foo is false. After the assigning foo to true for the second record as I describe above, the result is R1, R3, R4, R2 and R2.foo is still false.

The workaround seems to be assigning cs[1] to a variable first then change the variable. It would be nice the know why.

Is there anything in Django that can cause queryset result to be rearranged?

Edit does each queryset[index] cause a database hit?

Upvotes: 1

Views: 155

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477190

The problem: qs is a QuerySet. If you write qs[i], you actually make another query (one that looks like <OLD QUERY> LIMIT 1 OFFSET i), so you fetch the i-th element from the old queryset into memory.

The point is that if you write qs[i] twice, you make two independent fetches. If we thus write two qs[1]s, we obtain two different objects. If you thus perform a qs[1].save(), you make an independent fetch, load the object from the database, and immediately save it. But previous modifications that have not been saved will have no effect.

If you thus want to make a change to an object, you need to use a reference, so write:

# will change the second element
cs1 = cs[1]     # we fetch the object, and store it in cs1
cs1.foo = True  # we change the object
cs1.save()      # we save the object

instead of:

# will *NOT* change the second element
cs[1].foo = True  # load object, change it, but throws the object away
cs[1].save()      # loads the object again, and save it, with no change

The fact that the order of the objects changes between two queries is common in many database engines (well usually given some row is updated, or even if a new row is created). It depends on the indexes a database has how the rows are obtained, and often rows that are changed will occur as last (or first) row in the result set, although if you do not specify the order, usually there are no strong guarantees how the result will be ordered.

Upvotes: 1

Related Questions