Skip to content

Instantly share code, notes, and snippets.

@jhgaylor
Last active May 7, 2024 19:54
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save jhgaylor/5873301 to your computer and use it in GitHub Desktop.
Save jhgaylor/5873301 to your computer and use it in GitHub Desktop.
Example of using a through model in Django and filtering by a value on the custom through model.
class A(models.Model):
things = models.ManyToManyField("B", through=ThroughModel)
class B(models.Model):
text = models.TextField()
class ThroughModel(models.Model):
a = models.ForeignKey(A)
b = models.ForeignKey(B)
extra = models.BooleanField()
#this will return a list of ThroughModel objects
ThroughModel.objects.filter(b=instance_of_b, extra=True)
#this will return a list of A objects based on an extra field on the through table
A.objects.filter(things__ThroughModel__extra=True)
#keep in mind that limiting by one of the foreign keys on the through model is easier
A.objects.filter(things=instance_of_b)
@nbeuchat
Copy link

Very useful snippet, thanks!

@Sajam
Copy link

Sajam commented Jun 28, 2018

A.objects.filter(things__ThroughModel__extra=True)

How is this possible? Which version of Django? I need it, but trying with 1.11.7 doesn't work. Thanks!

@Palisand
Copy link

Palisand commented Nov 28, 2018

I'm on 2.1.3 and A.objects.filter(throughmodel__extra=True) works for me.

@sheikhsalman08
Copy link

great 👍

@nlothian
Copy link

After spending some time on this, I'd second what @Palisand said here:

A.objects.filter(throughmodel__extra=True) is the correct syntax in Django 2.2. Note that is is lowercase.

@matt-fff
Copy link

matt-fff commented Dec 11, 2019

Thanks a lot - exactly what I needed. I did have to use the lowercase version for 2.2.

@erosenzweig
Copy link

This post really helped me a lot! But I was still missing how to pull these through an API. So here is an example that builds upon yours that uses the many-to-many relationship of Books and Authors with the extra fields extra and external_url in ThroughModel we want to pull through an DRF endpoint. It also lists serializers, views and how things are connected. Hopefully others will find it useful despite the terrible naming :)

Assuming the data in the database looks like this:

Authors

| id | name                 | biography                                                                                                                                               | date_of_birth |
|----|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| 1  | Daniel Gerhard Brown | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s | 1964-06-02    |
| 2  | Bob Brown Smith      | Lorem Ipsum is simply dummy text of the printing and typesetting industry.                                                                              | 1978-04-03    |
| 3  | Jane Doe             | Lorem Ipsum is simply dummy text of the.                                                                                                                | 1998-06-04    |
| 4  | Daniel Crawford      | Lorem Ipsum is simply dummy text of the.                                                                                                                | 1966-06-06    |

Books

| id | title             | description                                                                                                                                             | publisher    | release_date |
|----|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|--------------|
| 1  | The Origin        | The origin is a 2017 mystery thriller novel by American utthor Dna Brown and the fifth installment in his Robert Langdon series.                        | Double Day   | 2017-10-03   |
| 2  | Angels and Demons | Angels & Demons is a 2000 bestselling mystery-thriller novel written by American author Dan Brown and published by Pocket Books and then by Corgi Books | Pocket Books | 2017-05-01   |
|    |                   |                                                                                                                                                         |              |              |

ThroughModel

| id | external_url       | extra | author_id | book_id |
|----|--------------------|-------|-----------|---------|
| 0  | https://github.com | true  | 1         | 1       |
| 1  | https://gitlab.com | false | 1         | 2       |

This is how the models, serializers, and views all connect together.

models.py

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=225)
    biography  = models.TextField()
    date_of_birth = models.DateField()
    books = models.ManyToManyField('Book', through='ThroughModel', blank=True, related_name='authors')

class Book(models.Model):
    title = models.CharField(max_length=100)
    description = models.CharField(max_length=400)
    publisher = models.CharField(max_length=400)
    release_date = models.DateField()

class ThroughModel(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='author_books')
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_authors')
    external_url = models.TextField()
    extra = models.BooleanField()`

serializers.py

from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Author, Book, ThroughModel

class UserSerializer(serializers.ModelSerializer):
	class  Meta:
		model = User
		fields = ('username', 'password')

class ThroughModelSerializer(serializers.ModelSerializer):
	# as of DRF 3.0 use serializers.ReadOnlyField instead of serializers.Field
	author = serializers.ReadOnlyField(source='author.id') 
	book = serializers.ReadOnlyField(source='book.id')
	
	class  Meta:
		model = ThroughModel
		fields = ('author', 'book', 'external_url', 'extra')
	
class AuthorSerializer(serializers.ModelSerializer):
	# note the use of the ThroughModelSerializer and the same field name as the related_name='author_books' in ThroughModel
	author_books = ThroughModelSerializer(many=True) 
	class  Meta:
		ordering = ['-id']
		model = Author
		fields = ("id", "name", "biography", "date_of_birth", "author_books")
		extra_kwargs = {'books': {'required': False}}

class BookSerializer(serializers.ModelSerializer):
	book_authors = ThroughModelSerializer(many=True)
	class  Meta:
		ordering = ['-id']
		model = Book
		fields = ("id", "title", "description", "publisher", "release_date", "book_authors")
		extra_kwargs = {'authors': {'required': False}}

views.py

from rest_framework import viewsets
from .serializer import UserSerializer, AuthorSerializer, BookSerializer
from .models import Author, Book
from rest_framework import filters
	
class AuthorViewSet(viewsets.ModelViewSet):
	queryset = Author.objects.all()
	serializer_class = AuthorSerializer

class BookViewSet(viewsets.ModelViewSet):
	queryset = Book.objects.all()
	serializer_class = BookSerializer
	filter_backends = [filters.OrderingFilter]
	ordering_fields = ['release_date']

Finally, this will give results that look like this:

query /api/authors/

[
   {
      "id":2,
      "name":"Bob Brown Smith",
      "biography":"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type.",
      "date_of_birth":"1978-04-03",
      "author_books":[
         
      ]
   },
   {
      "id":3,
      "name":"Jane Doe",
      "biography":"Lorem Ipsum is simply dummy text of the.",
      "date_of_birth":"1998-06-04",
      "author_books":[
         
      ]
   },
   {
      "id":4,
      "name":"Daniel Crawford",
      "biography":"Lorem Ipsum is simply dummy text of the.",
      "date_of_birth":"1966-06-06",
      "author_books":[
         
      ]
   },
   {
      "id":1,
      "name":"Daniel Gerhard Brown",
      "biography":"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
      "date_of_birth":"1964-06-02",
      "author_books":[
         {
            "author":1,
            "book":1,
            "external_url":"https://github.com",
            "extra":true
         },
         {
            "author":1,
            "book":2,
            "external_url":"https://gitlab.com",
            "extra":false
         }
      ]
   }
]

query /api/books/

[
   {
      "id":1,
      "title":"The Origin",
      "description":"The origin is a 2017 mystery thriller novel by American utthor Dna Brown and the fifth installment in his Robert Langdon series.",
      "publisher":"Double Day",
      "release_date":"2017-10-03",
      "book_authors":[
         {
            "author":1,
            "book":1,
            "external_url":"https://github.com",
            "extra":true
         }
      ]
   },
   {
      "id":2,
      "title":"Angels and Demons",
      "description":"Angels & Demons is a 2000 bestselling mystery-thriller novel written by American author Dan Brown and published by Pocket Books and then by Corgi Books",
      "publisher":"Pocket Books",
      "release_date":"2017-05-01",
      "book_authors":[
         {
            "author":1,
            "book":2,
            "external_url":"https://gitlab.com",
            "extra":false
         }
      ]
   }
]

@domanskyi
Copy link

Oh God, I've been looking for this for 3 hours!

@jhgaylor
Copy link
Author

I am glad that people have been able to comment on this to keep it up to date. I had no idea it would still be helping people years later. I am glad it was helpful :)

@hepcat72
Copy link

hepcat72 commented Mar 11, 2022

Someone told me on stack that if all you have is:

class A(models.Model):
    things = models.ManyToManyField("B")
class B(models.Model):
    text = models.TextField()

(i.e. no extra fields for the relationship), there was a syntax to use in A.objects.filter(...) to reference the "hidden" through model, in the corresponding way that it can be accessed using:

qs = A.things.through.objects.filter(Q(b__text__icontains="whatever"))

^^^ I know the above works without defining through. I've tried it multiple times as I was trying to work all this out. But I was unable to find a corresponding way off the A model directly, such as:

qs = A.objects.filter(things__b__text__icontains="whatever")

^^^ which is wrong. So does there exist a way to reference the hidden through model in the filter expression from the class containing the M:M relationship (without explicitly defining through)? I don't think there is, but you guys seem to know this topic well, so I suspect someone here knows...

I'm just curious. I figured out how to do what I wanted to do and learned a lot about all this, but I'm just wondering if the suggestion I received on stack was wrong - or whether I just couldn't figure it out...

@linhfishCR7
Copy link

linhfishCR7 commented Dec 18, 2022

A.objects.filter(throughmodel__extra=True)
It does not work for version 4.0.5
Please help me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment