1
class Order(models.Model):
   name = models.CharField(max_length=100)
   # other fields..
   user = models.ForeginKey(User)
   old = models.BooleanField(default=False)

I want to display all the orders of a specific user, but I want to split them those which are "old" and the ones who are not.

So, currently I do in views.py:

orders = Order.objects.filter(user=user)

In template:

First table:

<table>
{% for order in orders %}
{% if not order.old %}
   <tr>
     <td>... </td> 
   </tr>
{% endif %}
{% endfor %}
</table>

And another table:

{% for order in orders %}
{% if order.old %}
   <tr>
     <td>...</td>
   <tr>
{% endif %}
{% endfor %}

This way have some drawbacks, first, now I want to count how many of the orders are "old", to display this number in the template. I can't, unless I do another query. Is it possible to annotate(number_of_old=Count('old'))? Or I have to do another query?

So what would be the best?
1. Do two queries, one with old=False, another with old=True, and pass two querysets to the template. And use |len filter on the querysets
2. Do one query like this and split them somehow in python? That will be less convenient as I have a similar structures which I want to split like that.

And should I call the DB .count() at all?

EDIT:
If I would write my model like this:

class Order(models.Model):
   name = models.CharField(max_length=100)
   # other fields..
   user = models.ForeginKey(User)
   old = models.BooleanField(default=False)
   objects = CustomManager() # Custom manager


class CustomQueryset(models.QuerySet):
    def no_old(self):
        return self.filter(old=False)

class CustomManager(models.Manager):
    def get_queryset(self):
        return CustomQuerySet(model=self.model, using=self._db)

Is this template code produce one or two queries ?

{% if orders.no_old %}
{% for order orders.no_old %}
... 
{% endfor %}
{% endif %} 
user3599803
  • 6,435
  • 17
  • 69
  • 130
  • Possible duplicate of [Django - Grouping querysets by a certain field in template](http://stackoverflow.com/questions/8678336/django-grouping-querysets-by-a-certain-field-in-template) – Sayse Jul 12 '16 at 12:25
  • saw the {% regroup %} tag, but the problem it's does not order its input. so to use it I will have to add an order_by('old'), which I don't need here. I've already have an ordering on my queryset. Not sure what will have better performance, according to django docs, ordering is not "free".. – user3599803 Jul 12 '16 at 12:30
  • How big is the average user's orders size? If we are talking about 5-10-50 orders per user you really should not think about which approach is better - 2 querysets vs 2 iterations - save your time for something more important because [premature optimisations is the root of all evil](http://c2.com/cgi/wiki?PrematureOptimization) – Todor Jul 12 '16 at 12:48
  • it can be of around size 100, but this is not the only model where I need to split it that way. I have around 5 similar models which I want to split by one field – user3599803 Jul 12 '16 at 12:59
  • If there is any chance to introduce a pagination now or in the future, then two querysets is the correct (and mb the only) way to go. – Todor Jul 12 '16 at 13:10

1 Answers1

1

You can't do any annotations, and there is no need to make .count() since you already have all the data in memory. So its really just between:

orders = Order.objects.filter(user=user)
old_orders = [o for o in orders if o.old]
new_orders = [o for o in orders if not o.old]

#or

old_orders = Order.objects.filter(user=user, old=True)
new_orders = Order.objects.filter(user=user, old=False)

In this specific scenario, I don't think there will be any performance difference. Personally I will choose the 2nd approach with the two queries.

A good read with tips about the problem: Django Database access optimization

Update

About the custom Manager which you introduce. I don't think you are doing it correctly I think what you want is this:

class CustomQueryset(models.QuerySet):
    def no_old(self):
        return self.filter(old=False)

class Order(models.Model):
   name = models.CharField(max_length=100)
   # other fields..
   user = models.ForeginKey(User)
   old = models.BooleanField(default=False)

   #if you already have a manager
   #objects = CustomManager.from_queryset(CustomQueryset)()
   #if you dont:
   objects = CustomQueryset.as_manager()

So having:

orders = Order.objects.filter(user=user)

If you do {% if orders.no_old %} will do another query, because this is new QuerySet instance which has no cache..

About the {% regroup %} tag

As you mention, in order to use it, you need to .order_by('old'), and if you have another order, you can still use it, just apply your order after the old, e.g. .order_by('old', 'another_field'). This way you will use only one Query and this will save you one iteration over the list (because Django will split the list iterating it only once), but you will get less readability in the template.

Community
  • 1
  • 1
Todor
  • 15,307
  • 5
  • 55
  • 62
  • It's just seem weird to query the same table using almost identical query.. – user3599803 Jul 12 '16 at 12:08
  • And If I would need only to count the number of old=True, without displaying them. would you still do two queries, one with .count() ? – user3599803 Jul 12 '16 at 12:09
  • Its more weird to me to iterate a single list 2 times just to split it. And then one more time in the template to render it. And yes, if I don't need that records into memory I will use `.count()`. – Todor Jul 12 '16 at 12:10
  • Thanks! I think I might go with a custom queryset/manager, since I want to avoid passing 2 querysets. I've added an edit to the post, can you share your opinion? – user3599803 Jul 12 '16 at 12:25
  • I see, but even with .as_manager(), I still think it will do two queries when used in template.. – user3599803 Jul 12 '16 at 12:57
  • Yes, to better understand when a QuerySet is cached and will not make another db query take a look at: [Caching and QuerySets](https://docs.djangoproject.com/en/1.9/topics/db/queries/#caching-and-querysets) – Todor Jul 12 '16 at 13:06
  • So I ended up using {% with %} in my template, to use it in the {% if %} and {% for %}, I've also considered the @cahced_property if anyone interested. – user3599803 Jul 12 '16 at 13:59