Make your Django Query Logic DRYer

Let’s say you’re making a database of music. It’s going to include Albums and Singles and Artists. Multiple artists could have participated on the same Song. Here’s a relatively simple data relationship:

from django.db import models
class Artist(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
class Album(models.Model):
name = models.CharField(max_length=50)
release_date = models.DateField()
class Song(models.Model):
artists = models.ManyToManyField(Artist)
albums = models.ManyToManyField(Album)
name = models.CharField(max_length=100)
release_date = models.DateField()

You’ll note it’s a pain in the butt to find out who sang a song. Let’s say you want to find the list of Artists in the Lion King. Your query may look something like this:

songs = Song.objects.filter(albums__name='Lion King') 
artists_from_lion_king = Artist.objects.filter(song_set__in=songs)

Admittedly, this is really bad logic, since it’s only useful for the Lion King. But it’s totally possible you might find yourself doing something like that in a view, where you’re passing in the name as parameter. But that’s dumb. Don’t do that bit code right above. Do this instead:

from django.db import models
class Artist(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
    @property
def albums(self):
songs = self.song_set.all()
return Album.objects.filter(song_set__in=songs)
class Album(models.Model):
name = models.CharField(max_length=50)
release_date = models.DateField()
    @property
def artists(self):
songs = self.song_set.all()
return Artist.objects.filter(song_set__in=songs)
class Song(models.Model):
artists = models.ManyToManyField(Artist)
albums = models.ManyToManyField(Album)
name = models.CharField(max_length=100)
release_date = models.DateField()

A little explanation as to the magic going on. You’re collecting the song_set related to each model, Album and Arists, and having a generalized function run to collect for you a queryset (note not a list, which makes this doubly awesome).

I do this pattern all the time. It’s extremely convenient.

However, I’m sure there are other options out there. How do you all keep your query logic in Django DRY?