Prevent Django N+1 queries and accidental lazy database fetches with explicit ORM policies
Project description
django-fetch-guard
Prevent Django N+1 queries and accidental lazy database fetches—with explicit contracts or automatic batching.
django-fetch-guard gives Django querysets an explicit fetch policy. Use
peers to collapse common N+1 patterns into batched queries, or use strict
to fail immediately when code touches a field or relation that was not loaded
intentionally.
It uses Django 6.1's native fetch modes when available and provides a carefully scoped compatibility engine for Django 4.2 through 6.0.
Why this exists
This innocent loop can perform 101 queries for 100 books:
books = Book.objects.all() # 1 query
for book in books:
print(book.author.name) # N more queries
Fetch guard lets the queryset state its contract:
# Batch related authors: usually 2 queries in total.
books = Book.objects.fetch_peers()
# Or require an explicit plan: 1 joined query, no hidden fetching allowed.
books = Book.objects.select_related("author").strict()
That contract follows the queryset into serializers, views, templates, and service functions instead of relying on every caller to remember a query-count assertion.
Compatibility
| Django version | fetch_peers() |
strict() |
|---|---|---|
| 6.1 | Native on-demand FETCH_PEERS |
Native RAISE mode |
| 4.2–6.0 | Prefetch direct or named relations | Compatibility model mixin |
The public API is the same across supported versions. See the compatibility guide for exact differences and honest limitations.
Installation
Install the package from PyPI:
python -m pip install django-fetch-guard
For Django REST Framework integration:
python -m pip install "django-fetch-guard[drf]"
Install the current development version directly from GitHub:
python -m pip install "django-fetch-guard @ git+https://github.com/yassinbahri/django-fetch-guard.git"
Contributors should use the editable setup in CONTRIBUTING.md.
Five-minute setup
1. Add the mixin and manager
from django.db import models
from fetch_guard import FetchGuardModelMixin, GuardedManager
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(FetchGuardModelMixin, models.Model):
title = models.CharField(max_length=150)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
objects = GuardedManager(default_mode="raise")
The mixin is needed only by the Django 4.2–6.0 strict compatibility engine, but keeping it makes the model portable. It adds no database fields and requires no migration.
2. Choose a mode
Book.objects.strict() # Block an unloaded field or relation.
Book.objects.fetch_peers() # Batch missing direct relations.
Book.objects.normal() # Restore traditional lazy loading.
Use explicit relations for consistent eager behavior on every version:
Book.objects.fetch_peers("author")
3. Make strict querysets complete
books = Book.objects.select_related("author").strict()
for book in books:
print(book.author.name) # Already loaded; no extra query.
Without select_related("author"), the relation access raises the portable
exception:
from fetch_guard import FieldFetchBlocked
Import this package exception rather than Django's exception directly. It maps to Django's native class on 6.1 and the compatibility class on older versions.
Project-wide defaults and checks
Add the application when you want settings validation:
INSTALLED_APPS = [
# ...
"fetch_guard",
]
FETCH_GUARD = {
"DEFAULT_MODE": "raise",
}
Then the manager can inherit the project setting:
objects = GuardedManager()
Validate it with:
python manage.py check --tag fetch_guard
Guard any queryset
An existing manager can remain unchanged:
from fetch_guard import guard_queryset
books = guard_queryset(Book.objects.filter(is_published=True), "raise")
For custom manager/queryset composition, see the getting-started guide.
Django REST Framework
from fetch_guard.drf import FetchGuardMixin
from rest_framework.viewsets import ModelViewSet
class BookViewSet(FetchGuardMixin, ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
fetch_guard = {
"list": "peers",
"retrieve": "raise",
"default": "raise",
}
def get_queryset(self):
queryset = super().get_queryset()
if self.action == "retrieve":
queryset = queryset.select_related("author")
return queryset
Read the DRF guide for resolution order, opt-outs, nested serializers, and queryset planning.
Pytest
The package registers a focused fixture:
import pytest
from fetch_guard import FieldFetchBlocked
@pytest.mark.django_db
def test_book_serializer(fetch_guard):
book = fetch_guard.strict(Book.objects.all()).first()
with pytest.raises(FieldFetchBlocked):
serialize_book(book)
The fixture affects only the queryset passed to it; it does not monkey-patch Django globally. See the testing guide.
Verified integration behavior
The test suite includes a minimal Django application and real HTTP server coverage. It measures the same serialization path under every policy:
| Policy | Queries for two books | Result |
|---|---|---|
| Normal | 3 | Demonstrates N+1 |
| Peers | 2 | Authors loaded in one batch |
Strict with select_related() |
1 | Explicit query plan succeeds |
| Strict without related loading | — | Fetch is blocked |
Run all unit and integration tests with:
python -m pip install -e ".[test]"
python -m pytest
Documentation
- Getting started and core API
- API reference
- Django compatibility and limitations
- Django REST Framework
- Testing
- Troubleshooting
- Contributing
- Code of Conduct
- Changelog
Current status
0.1.0 is the initial alpha release. The core API, legacy compatibility engine, DRF mixin,
pytest fixture, system check, and cross-version CI matrix are implemented. The
tests include Django's request client and a live HTTP server.
Rich call-site diagnostics and production audit sampling are planned for later
releases.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_fetch_guard-0.1.0.tar.gz.
File metadata
- Download URL: django_fetch_guard-0.1.0.tar.gz
- Upload date:
- Size: 1.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e330316bb0d28ff31ae3fa72705b3814cdc326029274176c0ababa8716d2bdde
|
|
| MD5 |
150550e7e90eb0d8a251bbc5e83b7e2e
|
|
| BLAKE2b-256 |
1a22b736356fde9b8a1a7c0790ea9e2c00d7424d978de5951f096c1ede113081
|
Provenance
The following attestation bundles were made for django_fetch_guard-0.1.0.tar.gz:
Publisher:
release.yml on yassinbahri/django-fetch-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_fetch_guard-0.1.0.tar.gz -
Subject digest:
e330316bb0d28ff31ae3fa72705b3814cdc326029274176c0ababa8716d2bdde - Sigstore transparency entry: 2284585443
- Sigstore integration time:
-
Permalink:
yassinbahri/django-fetch-guard@5576b9d2b98be4746693b4626cefef29b0539b0f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/yassinbahri
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5576b9d2b98be4746693b4626cefef29b0539b0f -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_fetch_guard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_fetch_guard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68a4b40e36c492f2dc7c6f2a8ef071ecf3e9b8f41ab46bb7f2f57ceae6f8fb64
|
|
| MD5 |
362d8545ab6e5301386607ebff9b24d5
|
|
| BLAKE2b-256 |
0cfd7c1c4a36fc99fcd6c9ddad0e4443efaddf266a1ebeb5e29f96dc322e8946
|
Provenance
The following attestation bundles were made for django_fetch_guard-0.1.0-py3-none-any.whl:
Publisher:
release.yml on yassinbahri/django-fetch-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_fetch_guard-0.1.0-py3-none-any.whl -
Subject digest:
68a4b40e36c492f2dc7c6f2a8ef071ecf3e9b8f41ab46bb7f2f57ceae6f8fb64 - Sigstore transparency entry: 2284585726
- Sigstore integration time:
-
Permalink:
yassinbahri/django-fetch-guard@5576b9d2b98be4746693b4626cefef29b0539b0f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/yassinbahri
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5576b9d2b98be4746693b4626cefef29b0539b0f -
Trigger Event:
push
-
Statement type: