Home > Net >  Counting many-to-many field returns wrong value (Django)
Counting many-to-many field returns wrong value (Django)

Time:03-31

I have a model class Student:

class Student(models.Model):
   ... 

and model class Course:

class Course(models.Model)
   students = models.ManyToManyField(Student)

I now want to filter Course based on number of Students associated with a course. I tried:

Course.objects.annotate(student_count = Count('students'))

But for some reason, student_count is always returning one.

Let's say I create a course and add two students to it:

s1 = Student.objects.create()
s2 = Student.objects.create()
m1 = Course.objects.create()
m1.students.add(s1)
m1.students.add(s2)

print(Course.objects.all().first().students.count())
print(Course.objects.annotate(student_count = Count('students')).first().student_count

Prints

2
1

Why are these two values different? How can I filter courses based on the number of Students?

CodePudding user response:

I tested your scenario and the result is the same for two approach:

class SOTestCase(TestCase):
    def setUp(self):
        s1 = Student.objects.create()
        s2 = Student.objects.create()
        m1 = Course.objects.create()
        m1.students.add(s1)
        m1.students.add(s2)

    @override_settings(DEBUG=True)
    def test_query(self):
        c1 = Course.objects.all().first()
        c2 = Course.objects.annotate(student_count = Count('students')).first()

        n1 = c1.students.count()
        n2 = c2.student_count

        self.assertEqual(n1, 2)
        self.assertEqual(n2, 2)

Result is OK:

Ran 1 test in 0.017s

OK

Maybe do you have ordering? Try removing ordering:

        c1 = (
            Course
            .objects
            .order_by() #<-- this one
            .first())
        c2 = (
            Course
            .objects
            .order_by() #<-- this one
            .annotate(student_count = Count('students'))
            .first())

CodePudding user response:

Your problem is coming from annotation part. Every time that you add annotation part to your queryset, you also add a group_by to your query and if you don't add specific group_by to your queryset it automatically adds it for you (i.e. group_by "id"). Then your results become smaller and it'll remove duplicates from your query results (because of group_by). For instance look at these querysets:

1- Course.objects.all()[3].students.count()

2- Course.objects.annotate(student_count=Count("students"))[3].student_count

Although I tried to get third result from both of my querysets, the final result is different, because length of these two querysets are different (Course.objects.all() and Course.objects.annotate(student_count=Count("students")). So if you look at these queries which are related to our querysets:

1- {'sql': 'SELECT *, COUNT("students"."id") AS "student_count" FROM "course" LEFT OUTER JOIN "students" ON ("course"."id" = "students"."course_id") GROUP BY "course"."id" ORDER BY "course"."id" ASC LIMIT 1 OFFSET 3'}


2- {'sql': 'SELECT COUNT(*) AS "__count" FROM "students" WHERE "students"."course_id" = 4'}

you can see the group by part in second query. And also note that you should use second one for proper result (except for some situations that you know the exact index of your desired result in first queryset). Otherwise there are always situations which you might get different or wrong results because your first or last record might change in annotations.

  • Related