Django Models + ORM Discipline
Model design, migrations, queryset hygiene, and N+1 prevention.
# CLAUDE.md — Django Models + ORM
## Model design
- One model per concept. Don't pile unrelated fields onto a single `User` or `Order`.
- Required fields have no default. `null=False, blank=False` is the default — keep it that way unless you mean otherwise.
- `null=True` only on database NULL columns. For string fields, use `blank=True` and `default=''` instead.
- Use `models.UUIDField(primary_key=True, default=uuid.uuid4)` when IDs leak to clients. Sequential IDs invite enumeration.
- Always set `verbose_name` for fields users will see in admin or generated docs.
## Migrations
- One migration per logical change. Never edit a migration after it's merged — add a new one.
- `makemigrations --dry-run` before committing. Confirm the diff is what you expected.
- Long-running data migrations go in a `RunPython` op with `atomic=False` and batched updates. Don't lock production for an hour.
- Always provide a reverse migration where possible. `RunPython.noop` is fine when the change is one-way.
## Querysets
- Use `select_related('fk_field')` for forward FKs and one-to-ones. `prefetch_related('reverse_set')` for reverse FKs and M2M.
- Profile every list endpoint with `django-debug-toolbar` or `django-silk`. N+1 queries are the most common Django bug.
- `only()` and `defer()` to fetch partial rows when the table is wide.
- `iterator()` for batch reads that don't fit in memory. `bulk_create` and `bulk_update` for batch writes.
## Custom managers
- Custom `Manager` for table-wide concerns (soft delete, tenant filtering).
- Custom `QuerySet` for chainable filters: `objects = MyQuerySet.as_manager()`.
- Encode invariants here, not in views. `Order.objects.unpaid()` is clearer than `Order.objects.filter(paid=False)` scattered everywhere.
## Indexes & constraints
- Add `models.Index` for fields you filter or order by frequently. Define them in `Meta.indexes`.
- `UniqueConstraint` for compound uniqueness — `unique_together` is deprecated.
- `CheckConstraint` for invariants (e.g., `quantity__gte=0`). Better than a `clean()` method that runs sometimes.
## Soft delete
- If you need it, override the default manager with `objects = SoftDeleteManager()` and add `all_objects = models.Manager()` for admin.
- Add a `deleted_at: DateTimeField(null=True)`. Don't add a boolean — timestamps tell you when.
- Don't soft-delete by default for everything. Most data should be hard-deleted.
## Don't
- Don't raise inside `save()`. Validate at the form/serializer.
- Don't put business logic in `pre_save` signals. They run in spooky places. Use explicit service functions.
- Don't fetch a row, mutate it, and `save()` in a loop. Use `update()` or `bulk_update()`.
- Don't disable `atomic_requests` to "fix" a deadlock. Find the actual lock contention.
Other Django templates
Django + DRF Rules
Django REST Framework conventions: viewsets, serializers, permissions.
Django + Celery Background Jobs
Celery task design, idempotency, retries, and Redis as broker.
Django Testing with pytest
pytest-django setup, fixtures, factories, and fast database tests.
Django Deployment (Docker + gunicorn)
Production deploy: Docker images, gunicorn, env management, static files.