Detect N+1s in your Django app
Project description
django-zeal
Catch N+1 queries in your Django project.
🔥 Battle-tested at Cinder
Features
- Detects N+1s from missing prefetches and from use of
.defer()/.only() - Friendly error messages like
N+1 detected on User.followers at myapp/views.py:25 in get_user - Configurable thresholds
- Allow-list
- Well-tested
- No dependencies
Acknowledgements
This library draws heavily from jmcarp's nplusone. It's not a fork, but a lot of the central concepts and initial code came from nplusone.
Installation
First:
pip install django-zeal
Then, add zeal to your INSTALLED_APPS and MIDDLEWARE. You probably
don't want to run it in production: I haven't profiled it but it will have a performance
impact.
if DEBUG:
INSTALLED_APPS.append("zeal")
MIDDLEWARE.append("zeal.middleware.zeal_middleware")
This will detect N+1s that happen in web requests. To catch N+1s in more places, read on!
Celery
If you use Celery, you can configure this using signals:
from celery.signals import task_prerun, task_postrun
from zeal import setup, teardown
from django.conf import settings
@task_prerun.connect()
def setup_zeal(*args, **kwargs):
setup()
@task_postrun.connect()
def teardown_zeal(*args, **kwargs):
teardown()
Tests
Django runs tests with DEBUG=False,
so to run zeal in your tests, you'll first need to ensure it's added to your
INSTALLED_APPS and MIDDLEWARE. You could do something like:
import sys
TEST = "test" in sys.argv
if DEBUG or TEST:
INSTALLED_APPS.append("zeal")
MIDDLEWARE.append("zeal.middleware.zeal_middleware")
This will enable zeal in any tests that go through your middleware. If you want to enable it in all tests, you need to do a bit more work.
If you use pytest, use a fixture in your conftest.py:
import pytest
from zeal import zeal_context
@pytest.fixture(scope="function", autouse=True)
def use_zeal():
with zeal_context():
yield
If you use unittest, add a custom test runner:
# In e.g. `myapp/testing/test_runners.py`
from zeal import setup as zeal_setup, teardown as zeal_teardown
from django.test.runner import DiscoverRunner
from unittest.runner import TextTestResult
class ZealTestResult(TextTestResult):
def startTest(self, test):
zeal_setup()
return super().startTest(test)
def addError(self, test, err) -> None:
zeal_teardown()
return super().addError(test, err)
def addFailure(self, test, err) -> None:
zeal_teardown()
return super().addFailure(test, err)
def addSuccess(self, test):
zeal_teardown()
return super().addSuccess(test)
class ZealTestRunner(DiscoverRunner):
def get_resultclass(self):
return ZealTestResult
# And in your settings:
TEST_RUNNER = (
"myapp.testing.test_runners.ZealTestRunner"
)
Generic setup
If you also want to detect N+1s in other places not covered here, you can use the setup and
teardown functions, or the zeal_context context manager:
from zeal import setup, teardown, zeal_context
def foo():
setup()
try:
# your code goes here
finally:
teardown()
@zeal_context()
def bar():
# your code goes here
def baz():
with zeal_context():
# your code goes here
Configuration
By default, any issues detected by zeal will raise a ZealError. If you'd
rather log any detected N+1s, you can set:
ZEAL_RAISE = False
N+1s will be reported when the same query is executed twice. To configure this threshold, set the following in your Django settings.
ZEAL_NPLUSONE_THRESHOLD = 3
To handle false positives, you can temporarily disable zeal in parts of your code using a context manager:
from zeal import zeal_ignore
with zeal_ignore():
# code in this block will not log/raise zeal errors
If you only want to ignore a specific N+1, you can pass in a list of models/fields to ignore:
with zeal_ignore([{"model": "polls.Question", "field": "options"}]):
# code in this block will ignore N+1s on Question.options
Finally, if you want to ignore N+1 alerts from a specific model/field globally, you can add it to your settings:
ZEAL_ALLOWLIST = [
{"model": "polls.Question", "field": "options"},
# you can use fnmatch syntax in the model/field, too
{"model": "polls.*", "field": "options"},
# if you don't pass in a field, all N+1s arising from the model will be ignored
{"model": "polls.Question"},
]
Comparison to nplusone
zeal borrows heavily from nplusone, but has some differences:
- zeal also detects N+1 caused by using
.only()and.defer() - it lets you configure your own threshold for what constitutes an N+1
- it has slightly more helpful error messages that tell you where the N+1 occurred
nplusonepatches the Django ORM even in production when it's not enabled. zeal does not!nplusoneappears to be abandoned at this point.- however, zeal only works with Django, whereas
nplusonecan also be used with SQLAlchemy.
Contributing
- First, install uv.
- Create a virtual env using
uv venvand activate it withsource .venv/bin/activate. - Run
make installto install dev dependencies. - To run tests, run
make test.
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_zeal-1.1.0.tar.gz.
File metadata
- Download URL: django_zeal-1.1.0.tar.gz
- Upload date:
- Size: 11.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86c35e5950768dcb07e4fc5a5ce3dad42e9b41c8b08fc05dd7447cc5e789826c
|
|
| MD5 |
da02200296d281951ae6a74925a57e43
|
|
| BLAKE2b-256 |
b0659fe5af41387c1706275368b4fe7838a2657b76736860a9c0db0f2d44d101
|
File details
Details for the file django_zeal-1.1.0-py3-none-any.whl.
File metadata
- Download URL: django_zeal-1.1.0-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f38eb52efc538c3b49fa5e6e0a6c4ee111035f73a539397559f3d5f095a3c7f
|
|
| MD5 |
1dc60fedcd81b21371895da9f358cf96
|
|
| BLAKE2b-256 |
a4f8f47ecf92bbffdcb36c250d6da922ac524d9410ab29282c868519e35c6209
|