Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Multiple COUNTs from two models away

I am attempting to create a profile page that shows the amount of dwarves that are assigned to each corresponding career. I have 4 careers, 2 jobs within each of those careers and of course many dwarves that each have a single job. How can I get a count of the number of dwarves in each of those careers? My solution was to hardcore the career names in the HTML and to make a query for each career but that seems like an excessive amount of queries.

Here's what I "want" to see:

Unassigned: 3
Construction: 2
Farming: 0
Gathering: 1

Here's my models. I add some complexity by not connecting Careers directly to my Dwarves model (they have connected by their jobs).

from django.contrib.auth.models import User
from django.db import models

class Career(models.Model):
    name = models.CharField(max_length = 64)

    def __unicode__(self):
        return self.name

class Job(models.Model):
    career = models.ForeignKey(Career)
    name = models.CharField(max_length = 64)
    career_increment = models.DecimalField(max_digits = 4, decimal_places = 2)
    job_increment = models.DecimalField(max_digits = 4, decimal_places = 2)

    def __unicode__(self):
        return self.name

class Dwarf(models.Model):
    job = models.ForeignKey(Job)
    user = models.ForeignKey(User)
    created = models.DateTimeField(auto_now_add = True)
    modified = models.DateTimeField(auto_now = True)
    name = models.CharField(max_length = 64)

    class Meta:
        verbose_name_plural = 'dwarves'

    def __unicode__(self):
        return self.name

EDIT 1 my view looks something like:

def fortress(request):
    careers = Career.objects.annotate(Count('dwarf_set'))
    return render_to_response('ragna_base/fortress.html', {'careers': careers})

and template:

{% for career in careers %}
    <li>{{ career.dwarf_set__count }}</li>
{% endfor %}

The error is:

Cannot resolve keyword 'dwarf_set' into field. Choices are: id, job, name

SOLUTION

view:

def fortress(request):
    careers = Career.objects.all().annotate(dwarfs_in_career = Count('job__dwarf'))
    return render_to_response('ragna_base/fortress.html', {'careers': careers})

template:

{% for career in careers reversed %}
    <li>{{ career.name }}: {{ career.dwarves_in_career }}</li>
{% endfor %}

EVEN BETTER SOLUTION

careers = Career.objects.filter(Q(job__dwarf__user = 1) | Q(job__dwarf__user__isnull = True)) \
    .annotate(dwarves_in_career = Count('job__dwarf'))

Don't forget to from django.db.models import Count, Q

What I like about the above solution was it not only returns careers that have dwarves working but even the careers that have none which was the next problem I encountered. Here's my view for completeness:

<ul>
{% for career in careers %}
    <li>{{ career.name }}: {{ career.dwarves_in_career }}</li>
{% endfor %}
</ul>
like image 306
TheLizardKing Avatar asked Oct 15 '22 10:10

TheLizardKing


2 Answers

Django's ORM isn't gonna make this uber-simple. The simple way is to do something like:

for career in Career.objects.all(): career.dwarf_set.all().count()

That will execute 1 query for each job (O(n) complexity).

You could try to speed that up by using Django's Aggregation feature, but I'm not entirely sure if it'll do what you need. You'd have to take a look.

The third option is to use custom SQL, which will absolutely get the job done. You just have to write it, and maintain it as your app grows and changes...

like image 89
Gabriel Hurley Avatar answered Oct 21 '22 04:10

Gabriel Hurley


Does this do what you want?

from django.db.models import Count
Career.objects.annotate(Count('dwarf'))

Now each career object should have a dwarf__count property.

like image 32
Daniel Roseman Avatar answered Oct 21 '22 04:10

Daniel Roseman