13

Because of the nature of my project, I find myself constantly taking slices out of querysets, like so:

Thread.objects.filter(board=requested_board_id).order_by('-updatedate')[:10]

But this leaves me with the problem of actually DOING stuff with the elements that I have selected, because any kind of .update() or .filter() won't work after slicing.

I know of several ways to get around it, but they are all messy and confusing and seriously degrade the readability of the code, especially when I have to do it so often.

What's the best way to get around this slice-filter limitation?

flatterino
  • 1,005
  • 11
  • 21
  • 4
    This is a little broad. Can you give an example of the operations you want to do on it, and maybe some of your current workarounds which we might be able to improve? – Two-Bit Alchemist Feb 04 '16 at 16:35
  • For example: `Thread.objects.filter(board=requested_board)[:5].update(title='whatever')`. This WOULD work if not for the [:5] slicing. A current workaround consists of using `itertools.chain`, but that gives back an `itertools` object, which then has to be converted to a list to even be sent as a template context from a view. Works but it's very messy and unreadable. – flatterino Feb 04 '16 at 16:37
  • 1
    [This answer](http://stackoverflow.com/a/4286144/2588818) covers that case, and is what I would suggest as well. Take the ids of your filtered & sliced queryset (using `value_list` or similar), and plug them back in to form a new query using `update`. A little ugly, but not Django's fault, as explained there (`UPDATE... WHERE... LIMIT...` is not OK in SQL). – Two-Bit Alchemist Feb 04 '16 at 16:42
  • Or you could use django-bulk-update, as explained [here](http://stackoverflow.com/a/28551658/3918774). – LostMyGlasses Feb 04 '16 at 16:43
  • 1
    @Two-BitAlchemist actually you can simplify that - you don't need to take the IDs, you can just pass the queryset directly into `filter(id__in=sliced_queryset)`. – Daniel Roseman Feb 04 '16 at 16:45
  • Would that double the number of database hits? My `itertools` solution doesn't, but it's uglier, and so far having to choose between ugliness and performance sucks a bit. – flatterino Feb 04 '16 at 16:45
  • 3
    No, it will do it via a subquery: still a performance hit, but not as much as a whole other query. – Daniel Roseman Feb 04 '16 at 16:50
  • @DanielRoseman wow, that's interesting! – LostMyGlasses Feb 04 '16 at 16:53
  • @Man, slicing does not evaluate a `QuerySet` unless you use the `step` parameter, so using the way Daniel Roseman suggested is quite efficient and readable. – Nikita Feb 04 '16 at 17:31
  • Cool, @DanielRoseman, did not realize that. – Two-Bit Alchemist Feb 04 '16 at 19:32
  • def get_queryset(self, request): limited_ids = Items.objects.order_by("-id").values_list("id", flat=True)[:3] qs = super().get_queryset(request).filter(id__in=limited_ids) return qs – ali reza May 25 '22 at 03:24
  • There is a solution to a similar issue [here](https://stackoverflow.com/a/29693386/12379095) – Love Putin Not War Jun 04 '22 at 12:06

1 Answers1

30

So far, based on the comments, I have found this solution by Daniel Roseman to be the least "ugly":

sliced_queryset = Somemodel.objects.filter(field='fieldvalue')[:5]

And then use id__in= to reference the sliced queryset objects' IDs:

Somemodel.objects.filter(id__in=sliced_queryset).update(field_to_update='whatever')

It works well, I've just tried it.

I wish Django had a more 'direct' way of doing this, but it's still pretty straightforward. If anyone has a better way, please post it and I will mark your answer as correct.


As a bit of extra advice, if you're using this to increment a field, like a 'views' field, you can self reference it cleanly like this:

from django.db.models import F

Somemodel.objects.filter(id__in=sliced_queryset).update(views=F('views')+1)
flatterino
  • 1,005
  • 11
  • 21