Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In a Django QuerySet, how to filter for "not exists" in a many-to-one relationship

I have two models like this:

class User(models.Model):     email = models.EmailField()  class Report(models.Model):     user = models.ForeignKey(User) 

In reality each model has more fields which are of no consequence to this question.

I want to filter all users who have an email which starts with 'a' and have no reports. There will be more .filter() and .exclude() criteria based on other fields.

I want to approach it like this:

users = User.objects.filter(email__like = 'a%')  users = users.filter(<other filters>)  users = ??? 

I would like ??? to filter out users who do not have reports associated with them. How would I do this? If this is not possible as I have presented it, what is an alternate approach?

like image 985
Krystian Cybulski Avatar asked Feb 12 '13 11:02

Krystian Cybulski


People also ask

Can I filter a QuerySet Django?

The filter() method is used to filter you search, and allows you to return only the rows that matches the search term.

How do I do a not equal in Django QuerySet filtering?

To do a not equal in Python Django queryset filtering, we can negate a equal with ~ . to call filter with the Q object negated with ~ to return all the Entry results that don't have id 3.

How do you use does not exist Django?

SELECT user.pk, user. email, NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0. user = user.pk) AS no_reports FROM user WHERE email LIKE 'a%' AND NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0. user = user.pk);

How can I filter a Django query with a list of values?

To filter a Python Django query with a list of values, we can use the filter method with in . to search Blog entries with pk set to 1,4 or 7 by calling Blog. objects. filter with the pk_in argument set to [1, 4, 7] .


2 Answers

Note: this answer was written in 2013 for Django 1.5. See the other answers for better approaches that work with newer versions of Django

Use isnull.

users_without_reports = User.objects.filter(report__isnull=True) users_with_reports = User.objects.filter(report__isnull=False).distinct() 

When you use isnull=False, the distinct() is required to prevent duplicate results.

like image 134
Alasdair Avatar answered Sep 28 '22 18:09

Alasdair


As of Django 3.0 you can now use expressions directly in a filter(), removing the unnecessary SQL clause:

User.objects.filter(     ~Exists(Reports.objects.filter(user__eq=OuterRef('pk'))),     email__startswith='a' ) 
SELECT user.pk, user.email FROM user WHERE NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk) AND email LIKE 'a%'; 

Docs:

  • Exists
  • OuterRef
  • Filtering on a Subquery() or Exists() expressions

For Django 1.11+ you can add EXISTS subqueries:

User.objects.annotate(     no_reports=~Exists(Reports.objects.filter(user__eq=OuterRef('pk'))) ).filter(     email__startswith='a',     no_reports=True ) 

This generates SQL something like this:

SELECT     user.pk,     user.email,     NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk) AS no_reports FROM user WHERE email LIKE 'a%' AND NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk); 

A NOT EXISTS clause is almost always the most efficient way to do a "not exists" filter.


like image 41
OrangeDog Avatar answered Sep 28 '22 20:09

OrangeDog