Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I use prefetch_related to cache filtered querysets?

I am using DRF to serialize some related models. In my toy example below, assume that each author can have a million books. Clearly doing a db query for all "good" books and then another db query for all "bad" books is inefficient.

This post [http://ses4j.github.io/2015/11/23/optimizing-slow-django-rest-framework-performance/] offered some suggestions regarding prefetch_related. But I found that this only helped when I made subsequent calls to .books.all() rather than .books.filter() as happens in the properties below.

Is there any automatic way in Django to cache the books queryset and not have subsequent filters to it hit the database again?

Here is some code:

models.py:

class Author(models.Model):
  name = models.CharField(max_length=100)

  @property
  def good_books(self):
    return self.books.filter(is_good=True)

  @property
  def bad_books(self):
    return self.books.filter(is_good=False)


class Book(models.Model):
  title = models.CharField(max_length=100)
  is_good = models.BooleanField(default=False)
  author = models.ForeignKey(Author, related_name="books")

serializers.py:

class BookSerializer(serializers.ModelSerializer):
  class Meta:
    model = Book
    fields = ("title",)

class AuthorSerializer(serializers.ModelSerializer):
  class Meta:
    model = Author
    fields = ("name", "good_books", "bad_books",)

  good_books = BookSerializer(many=True, read_only=True, source="good_books")
  bad_books = BookSerializer(many=True, read_only=True, source="bad_books")

  @staticmethod
  def setup_eager_loading(queryset):
    queryset = queryset.prefetch_related("books") 
    return queryset

views.py:

class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
  serializer = AuthorSerializer

  def get_queryset(self):
    queryset = Author.objects.all()
    queryset = self.get_serializer_class().setup_eager_loading(queryset)
    return queryset

Thanks.


edit:

Using Prefetch:

@staticmethod
def setup_eager_loading(queryset):
  queryset = queryset.prefetch_related(
    Prefetch("books", queryset=Book.objects.filter(is_good=True), to_attr="good_books"),
    Prefetch("books", queryset=Book.objects.filter(is_good=False), to_attr="bad_books"),
  )
  return queryset

This still gives me extra db hits for the calls to filter.

like image 410
trubliphone Avatar asked Oct 31 '25 18:10

trubliphone


1 Answers

Instead of doing it in model's property, which will be evaluated for each author separately, you can do prefetch on view level and use Prefetch with to_attr argument:

class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
  serializer = AuthorSerializer

  def get_queryset(self):
    queryset = Author.objects.prefetch_related(
      Prefetch('books', queryset=Book.objects.filter(is_good=True), to_attr='good_books'), 
      Prefetch('books', queryset=Book.objects.filter(is_good=False), to_attr='bad_books')
    )
    return queryset
like image 163
neverwalkaloner Avatar answered Nov 04 '25 04:11

neverwalkaloner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!