Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to left outer join with extra condition in Django

I have these three models:

class Track(models.Model):
    title = models.TextField()
    artist = models.TextField()

class Tag(models.Model):
    name = models.CharField(max_length=50)

class TrackHasTag(models.Model):
    track = models.ForeignKey('Track', on_delete=models.CASCADE)
    tag = models.ForeignKey('Tag', on_delete=models.PROTECT)

And I want to retrieve all Tracks that are not tagged with a specific tag. This gets me what I want: Track.objects.exclude(trackhastag__tag_id='1').only('id') but it's very slow when the tables grow. This is what I get when printing .query of the queryset:

SELECT "track"."id" 
FROM   "track" 
WHERE  NOT ( "track"."id" IN (SELECT U1."track_id" AS Col1 
                              FROM   "trackhastag" U1 
                              WHERE  U1."tag_id" = 1) )

I would like Django to send this query instead:

SELECT "track"."id" 
FROM   "track" 
       LEFT OUTER JOIN "trackhastag" 
                    ON "track"."id" = "trackhastag"."track_id" 
                       AND "trackhastag"."tag_id" = 1 
WHERE  "trackhastag"."id" IS NULL; 

But haven't found a way to do so. Using a Raw Query is not really an option as I have to filter the resulting queryset very often.

The cleanest workaround I have found is to create a view in the database and a model TrackHasTagFoo with managed = False that I use to query like: Track.objects.filter(trackhastagfoo__isnull=True). I don't think this is an elegant nor sustainable solution as it involves adding Raw SQL to my migrations to mantain said view.

This is just one example of a situation where we need to do this kind of left join with an extra condition, but the truth is that we are facing this problem in more parts of our application.

Thanks a lot!

like image 539
Enric Calabuig Avatar asked Jul 04 '18 13:07

Enric Calabuig


People also ask

What does .values do in Django?

values() Returns a QuerySet that returns dictionaries, rather than model instances, when used as an iterable. Each of those dictionaries represents an object, with the keys corresponding to the attribute names of model objects.

What is the significance of on condition in an outer join?

A left join would return all the rows from table a , and for each row the matching row in table b , if it exists - if it doesn't, null s would be returned instead of b 's columns. The on clause defines how this matching is done.

How do you do join operations in Django ORM?

Join can be done with select_related method: Django defines this function as Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query.

What is the outer join in SQL?

Outer joins are joins that return matched values and unmatched values from either or both tables. There are a few types of outer joins: LEFT JOIN returns only unmatched rows from the left table, as well as matched rows in both tables.


1 Answers

As mentioned in Django #29555 you can use FilteredRelation for this purpose since Django 2.0.

Track.objects.annotate(
    has_tag=FilteredRelation(
        'trackhastag', condition=Q(trackhastag__tag=1)
    ),
).filter(
    has_tag__isnull=True,
)
like image 180
Simon Charette Avatar answered Oct 21 '22 16:10

Simon Charette