Django many-to-many making too many calls

Viewed 129

I have a simple m2m relationship as below:

class Category(ModelBase):
    name = models.CharField(max_length=255)
    icon = models.CharField(max_length=50)


class Course(ModelBase):
    name = models.CharField(max_length=255, unique=True)       
    categories = models.ManyToManyField(Category, related_name="courses")

I am using ListView to show all the courses in a category or all courses if no category provided.

views.py

class CourseListView(ListView):


   model = Course
    paginate_by = 15
    template_name = "courses.html"
    context_object_name = "courses"

    def get_queryset(self):
        queryset = (
            super()
            .get_queryset()
            .select_related("tutor")
            .prefetch_related("categories")
            .filter(active=True)
        )
        category_id = self.kwargs.get("category_id")
        return (
            queryset
            if not category_id
            else queryset.filter(categories__in=[category_id])
        )

    def get_context_data(self, *args, **kwargs: Any) -> Dict[str, Any]:
        context = super().get_context_data(**kwargs)
        category_id = self.kwargs.get("category_id")
        if category_id:
            context["current_category"] = Category.objects.get(id=category_id)
        context["categories"] = Category.objects.all()
        return context

Django is making duplicate calls as I am doing something like this in the template.

<div class="icon"><span class="{{ course.categories.first.icon }}"></span></div>

Not sure why, help much appreciated. Thanks!

3 Answers

When you do .prefetch_related('categories') the result of this prefetch will be used when you access course.categories.all. Any other queryset on course.categories will do a fresh query. Since course.categories.first is a new queryset, it does not use the prefetched result.

What you want to access in your template is the first result from course.categories.all(). But this is not easy in the template. I would recommend a method on the Course model:

class Course(...):
    ...

    def first_category(self):
        # Equivalent to self.categories.first(), but uses the prefetched categories
        categories = self.categories.all()
        if len(categories):
            return categories[0]
        else:
            return None

And then in your template you can call this method

<div class="icon"><span class="{{ course.first_category.icon }}"></span></div>

You can also access the first value like:

{{ course.categories.all.0.icon }}

It is not necessary to write a method.

because categories is ManyToMany which means one category may appear in many courses, but in the template you just calling the first category's icon, so there maybe more than two course with the same first category, and it will retrieve them all, i recommend using another for loop to loops through categories too.

{% for course in courses %}
<div>
    <h1>{{ course.name</h1>
    ......
    <h4>categories</h4>
    {% for category in course.categories %}
         <div class="icon"><span class="{{ category.icon }}"></span></div>
    {% endfor %}
</div>
{% endfor %}
Related