ozo
ozo

Reputation: 943

Why do Get and Filter give different results? (Django)

I am developing an app where students can evaluate their teachers. I have several models, but the important ones for this question are these:

class Professor(models.Model):
    name = models.CharField(max_length=50,null=True)
    categories = models.ManyToManyField(Category, related_name='professors')
    def __str__(self):
        return self.name

class Student(models.Model):
    name = models.CharField(max_length=50,null=True)
    professors = models.ManyToManyField(Professor, related_name='students',through='Studentprofesor' )
    def __str__(self):
        return self.name

class Studentprofesor(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    professor =  models.ForeignKey(Professor, on_delete=models.CASCADE)
    tested = models.BooleanField(default=False)

As far as I knew the main difference between get and filter was that I couldn't use get when there were several objects with the features I was looking for. But apart from that, they worked in a similar way. get for a single object, filter for several objects.

However, in this case I get different results when I run get and filter.

if I use get:

Student.objects.get(name="Mike").professors.all()

I obtain:

<QuerySet [<Professor: Tom>, <Professor: Jenn>]>

But if I use filter:

Student.objects.filter(name="Mike").professors.all()

I obtain:

AttributeError: 'QuerySet' object has no attribute 'professors'

it's as if filter is not able to follow the manytomany relationship between the objects.

Why is this happening?

Upvotes: 2

Views: 1012

Answers (2)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476534

There are huge differences between .get(..) and .filter(..). In short: .get(..) obtains a single model instance that satisfies the given conditions whereas .filter(..) filters the queryset and produces a queryset that conceptually contains model instances (!) that satisfy the given conditions.

Django's .get(..)

.get means that you aim to retrieve exactly one instance. So that means that if you write:

Model.objects.get(..)

the result is a Model instance (given there is such instance). As a result we can thus obtain attributes (like .professors, etc.) from that single entity. We have guarantees if the call succeeds that: (1) there are no multiple objects for which the filter criteria hold; and (2) there is at least one element for which the filter criteria hold. So the output is always a model instance, not a None or a QuerySet.

The .get(..) function is evaluated eagerly: we immediately perform a query to the database. In case the database returns no entries, or two or more, the Model.DoesNotExist and MultipleObjectsReturned exceptions are respectively risen.

Note: since .get(..) acts eagerly, adding filters, etc. at the right of .get(..) has no use (well unless you define a filter function on that instance, but that is not a good idea either). You can however use functions like .values(), values_list, prefetch_related, etc. on the left side to change the type of output (and prefetch certain parts). For example:

Student.objects.values().get(name='Mike')

will result in a dictionary containing the values of that instance.

Django's .filter(..)

Filter on the other hand filters a queryset. That means that it is possible that after we filter, the queryset no longer contains any instances (if the filter is too restrictive) or two ore more (if the filter is too weak to pin to a single entry).

Django does not evaluate such .filter(..) eagerly. This means that by default Django will not make a query to the database to retrieve entries. Only if you call for example len(..) over the resulting queryset, or you iterate over it, Django will first perform a database query and then handle the corresponding result.

Since the result of a .filter(..) is another QuerySet, we can chain operations together. For example, we can call an extra .filter(..) or .exclude(..), values_list(..) or any other function supported by the QuerySet.

Since the result is not a model instance, we can not call the attributes of a model instance. What should be the outcome of a Student.objects.filter(..).name in case no student matches the criteria? Or what if there are multiple Students that match the given constraint?

We can however obtain the list of Professors that teach one or more Students with the name 'Mike' with:

# professors with a student called Mike
Professors.objects.filter(students__name="Mike")

A filter will never raise an Model.DoesNotExist or a MultipleObjectsReturned exception, since it is perfectly allowed to work with empty QuerySets, or QuerySets with multiple items.

Upvotes: 2

neverwalkaloner
neverwalkaloner

Reputation: 47354

Bacause filter() returns queryset (multiple students). But professors is attribute of single student instance. You can use first() with filter() to get single object:

Student.objects.filter(name="Mike").first().professors.all()

Upvotes: 2

Related Questions