TheLizardKing
TheLizardKing

Reputation: 2094

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>

Upvotes: 5

Views: 981

Answers (3)

Daniel Roseman
Daniel Roseman

Reputation: 599490

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.

Upvotes: 1

Gabriel Hurley
Gabriel Hurley

Reputation: 40052

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...

Upvotes: 2

Rob Farley
Rob Farley

Reputation: 15849

Can't you just get a count grouped by career? And do an outer join if you need the zero rows returned too.

Upvotes: 1

Related Questions