[Django]-Cannot update a query once a slice has been taken

32👍

As the error states, you cannot call update() on a QuerySet if you took out a slice.

The reason:

  1. Taking a slice is equivalent to a LIMIT statement in SQL.
  2. Issuing an update turns your query into an UPDATE statement.

What you are trying to do would be equivalent to

UPDATE ... WHERE ... LIMIT 5

which is not possible, at least not with standard SQL.

71👍

The documentation suggests that something like the following might be possible – I’m not sure if doing the limiting in an inner QuerySet bypasses the check around calling update() after slicing:

inner_q = UserLog.objects.filter(user=user,
                                 action='message',
                                 timestamp__lt=now).values('pk')[0:5]
UserLog.objects.filter(pk__in=inner_q).update(read=True)

Failing that, you could use the in field lookup like so:

ids = UserLog.objects.filter(user=user,
                             action='message',
                             timestamp__lt=now).values_list('pk', flat=True)[0:5]
UserLog.objects.filter(pk__in=list(ids)).update(read=True)

5👍

Since Django 2.2 you can use bulk updates :

queryset = UserLog.objects.filter(user=user).filter(action='message').filter(timestamp__lt=now)
bulk = []
for userlog in queryset[0:5]:
    userlog.read = True
    bulk.append(userlog)
UserLog.objects.bulk_update(bulk,['read'])

2👍

I was getting the same error when attempting to limit the number of records returned by a queryset.

I found that if we’re using one of Django’s class-based generic views such as the ArchiveIndexView, we can use the paginate_by = attribute to limit the number of records.

For example (in views.py):

from django.views.generic import ArchiveIndexView
from .models import Entry

class HomeListView(ArchiveIndexView):
    """ Blog Homepage """
    model = Entry
    date_field = 'pub_date' 
    template_name = 'appname/home.html'
    queryset = Entry.objects.filter(
        is_active=True).order_by('-pub_date', 'title')
    paginate_by = 30

0👍

If you want to slice out some of the results of a queryset, you can copy it
it to another variable (a shallow copy is enough, which is faster
than a deep copy because it just uses references to the original
objects.)

import copy

queryset = Mytable.objects.all()
pieceOfQuery = copy.copy(queryset)
pieceOfQuery = pieceOfQuery[:10]

This will keep Django from complaining if you have an order_by filter on
your table, since that happens after the slicing if you do it on the main
queryset object

0👍

I think this answer (https://stackoverflow.com/a/4286144/12120968) is the way to go. But if you’re worried about race conditions, here’s another alternative.

There’s a select_for_update() method that locks rows when used inside a transaction. Here’s [

the link to the docs

]1 and a related post in StackOverlflow: Cannot update a query once a slice has been taken

So, for your use case, it would be something like this:

from typing import List
from django.db import transaction


with transaction.atomic():
    items_you_want_to_update: List[UserLog] = (
        UserLog.objects.select_for_update().filter(
            user=user,
            action='message',
            timestamp__lt=now
        )[:5] 
        # slicing evaluates the queryset and returns a list:
        # those row are now locked because of select_for_update()
    )
    
    for item in items_you_want_to_update:
        item.read = True

    # Using bulk_update() instead of .save() on each item to get better performance
    UserLog.objects.bulk_update(items_you_want_to_update)

-1👍

You can’t do that. From the Django documents: QuerySet API reference – update

-4👍

Your code is incorrect because of where the slicing happens. It should happen after the call to update(), not before.

Wrong:

UserLog.objects.filter(user=user).filter(action='message').filter(timestamp__lt=now)[0:5].update(read=True)

Right:

UserLog.objects.filter(user=user).filter(action='message').filter(timestamp__lt=now).update(read=True)[0:5]

Leave a comment