Skip to main content

Automatically prefetch foreign key values as needed.

Project description

https://img.shields.io/github/actions/workflow/status/tolomea/django-auto-prefetch/main.yml.svg?branch=main&style=for-the-badge https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge https://img.shields.io/pypi/v/django-auto-prefetch.svg?style=for-the-badge https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge pre-commit

Automatically prefetch foreign key values as needed.

Purpose

When accessing a ForeignKey or OneToOneField (including in reverse) on a model instance, if the field’s value has not yet been loaded then auto-prefetch will prefetch the field for all model instances loaded by the same QuerySet as the current model instance. This is enabled at the model level and totally automatic and transparent for users of the model.

Requirements

Python 3.9 to 3.14 supported.

Django 4.2 to 6.0 supported.

Usage

  1. Install with python -m pip install django-auto-prefetch.

  2. Change all these imports from django.db.models to auto_prefetch:

    • ForeignKey

    • Manager

    • Model - including inheriting Meta from auto_prefetch.Model.Meta

    • OneToOneField

    • QuerySet

    If you use custom subclasses of any of these classes, you should be able to swap for the auto_prefetch versions in your subclasses’ bases.

    For example, if you had:

    from django.db import models
    
    
    class Book(models.Model):
        author = models.ForeignKey("Author", on_delete=models.CASCADE)
    
        class Meta:
            verbose_name = "Book"

    …swap to:

    import auto_prefetch
    from django.db import models
    
    
    class Book(auto_prefetch.Model):
        author = auto_prefetch.ForeignKey("Author", on_delete=models.CASCADE)
    
        class Meta(auto_prefetch.Model.Meta):
            verbose_name = "Book"
  3. Run python manage.py makemigrations to generate migrations for all the models you modified. These migrations will set the Meta.base_manager_name option to prefetch_manager for every model that you’ve converted. This change ensures that auto-prefetching happens on related managers. Such migrations do not change anything in the database.

    (If you instead set Meta.base_manager_name on your models, make sure it inherits from auto_prefetch.Manager.)

Background and Rationale

Currently when accessing an uncached foreign key field, Django will automatically fetch the missing value from the database. When this occurs in a loop it creates 1+N query problems. Consider the following snippet:

for choice in Choice.objects.all():
    print(choice.question.question_text, ":", choice.choice_text)

This will do one query for the choices and then one query per choice to get that choice’s question.

This behavior can be avoided with correct application of prefetch_related() like this:

for choice in Choice.objects.prefetch_related("question"):
    print(choice.question.question_text, ":", choice.choice_text)

This has several usability issues, notably:

  • Less experienced users are generally not aware that it’s necessary.

  • Cosmetic seeming changes to things like templates can change the fields that should be prefetched.

  • Related to that, the code that requires the prefetch_related() (e.g. the template) may be quite removed from where the prefetch_related() needs to be applied (e.g. the view).

  • Subsequently finding where prefetch_related() / select_related() calls are missing is non-trivial and needs to be done on an ongoing basis.

  • Excess entries in prefetch_related() calls are even harder to find and result in unnecessary database queries.

  • It is very difficult for libraries like the admin and Django Rest Framework to automatically generate correct prefetch_related() clauses.

On the first iteration of the loop in the example above, when we first access a choice’s question field, instead of fetching the question for just that choice, auto-prefetch will speculatively fetch the questions for all the choices returned by the QuerySet. This change results in the first snippet having the same database behavior as the second while reducing or eliminating all of the noted usability issues.

Some important points:

  • ManyToManyFields are not changed at all.

  • Because these are ForeignKey and OneToOneFields, the generated queries can’t have more result rows than the original query and may have less. This eliminates any concern about a multiplicative query size explosion.

  • This feature will never result in more database queries as a prefetch will only be issued where the ORM was already going to fetch a single related object.

  • Because it is triggered by fetching missing related objects it will not at all change the DB behavior of code which is fully covered by prefetch_related() and/or select_related() calls.

  • This will inherently chain across relations like choice.question.author. The conditions above still hold under such chaining.

  • In some rare situations it may result in larger data transfer between the database and Django (see below).

An example of that last point is:

qs = Choice.objects.all()
list(qs)[0].question

Such examples generally seem to be rarer and more likely to be visible during code inspection (vs {{ choice.question }} in a template). And larger queries are usually a better failure mode than producing hundreds of queries. For this to actually produce inferior behavior in practice you need to: * fetch a large number of choices * filter out basically all of them * …in a way that prevents garbage collection of the unfiltered ones

If any of those aren’t true then automatic prefetching will still produce equivalent or better database behavior than without.

See Also

P.S.

If you have concerns go look at the code, it’s all in auto_prefetch/__init__.py and is fairly short.

History

django-auto-prefetch was written by Gordon Wrigley and is maintained by Adam Johnson.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_auto_prefetch-1.14.0.tar.gz (8.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_auto_prefetch-1.14.0-py3-none-any.whl (7.1 kB view details)

Uploaded Python 3

File details

Details for the file django_auto_prefetch-1.14.0.tar.gz.

File metadata

  • Download URL: django_auto_prefetch-1.14.0.tar.gz
  • Upload date:
  • Size: 8.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for django_auto_prefetch-1.14.0.tar.gz
Algorithm Hash digest
SHA256 7a8bef68ee3d2dc9a3f570d16aa3bd5102c82a2fced9efa40d5327cc3db3f856
MD5 ea8801179c3346ed4d5c7778558a5a80
BLAKE2b-256 c823d024935758e0a97f4e7a51b874670490c972704ca3e7d321e4b048a70c78

See more details on using hashes here.

File details

Details for the file django_auto_prefetch-1.14.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_auto_prefetch-1.14.0-py3-none-any.whl
Algorithm Hash digest
SHA256 223031c9e83aca44a7bb3f76ca9e74f3d806e4a0d979c7e0d630c4adcd41251f
MD5 42cc411ba71f3bbbe60375760f3a3ecc
BLAKE2b-256 f862a9dbe9e1034038bc3fd5a8a6a8f037d21f784fb9d9a32be81cb27daaca5b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page