2026-04-15 17:11:42 -05:00

142 lines
5.7 KiB
Markdown

---
name: django-models
description: Django model design patterns emphasizing fat models/thin views, QuerySet optimization, and domain logic encapsulation. Use when designing models, optimizing queries, implementing business logic, or working with the ORM.
---
# Django Model Patterns
## Core Philosophy: Fat Models, Thin Views
**Business logic belongs in models and managers, not views.** Views orchestrate workflows; models implement domain behavior. This principle creates testable, reusable code that stays maintainable as complexity grows.
**Good**: Model methods handle business rules, state transitions, validation
**Bad**: Views contain if/else logic for domain rules, calculate derived values
## Model Design
### Structure Your Models Around Domain Concepts
- Use `TextChoices`/`IntegerChoices` for status fields and enums
- Add `get_absolute_url()` for canonical object URLs
- Include `__str__()` for readable representations
- Set proper `ordering` in Meta for consistent default sorting
- Add database indexes for frequently filtered/sorted fields
- Use abstract base models for shared fields (timestamps, soft deletes, etc.)
### Field Selection Guidelines
- Use `blank=True, default=""` for optional text fields (avoid null)
- Use `null=True, blank=True` for optional foreign keys
- For unique optional fields, use `null=True` to avoid collision issues
- Leverage `JSONField` for flexible metadata (avoid creating many optional fields)
- Set appropriate `max_length` based on actual data needs
### Encapsulate Business Logic in Model Methods
- State transitions: `post.publish()`, `order.cancel()`
- Permission checks: `post.is_editable_by(user)`
- Complex calculations: `invoice.calculate_total()`
- Use properties for computed read-only values
- Specify `update_fields` when saving partial changes
## QuerySet Patterns: The Power of Composition
**Custom QuerySet classes are your secret weapon.** They make queries reusable, chainable, and testable.
### Pattern: QuerySet as Manager
```
Define a QuerySet subclass with domain-specific filter methods
Attach it to your model: objects = YourQuerySet.as_manager()
Chain methods for composable queries
```
### Benefits
- Reusable query logic across views, tasks, management commands
- Chainable methods enable expressive, readable queries
- Easy to test in isolation
- Encapsulates query complexity away from views
### Common QuerySet Methods
- Filtering by status/state
- Date range queries (recent, upcoming, expired)
- User-scoped queries (owned_by, visible_to)
- Combined lookups (published_and_recent)
## Query Optimization: Avoid N+1 Queries
### The Golden Rules
1. **select_related()**: Use for ForeignKey and OneToOneField (creates SQL JOIN)
2. **prefetch_related()**: Use for ManyToManyField and reverse ForeignKeys (separate query + Python join)
3. **only()**: Load specific fields when you don't need the whole object
4. **defer()**: Exclude heavy fields (TextField, JSONField) you won't use
5. **Prefetch()** object: Customize prefetch with filters and select_related
### Efficient Counting and Existence Checks
- Use `.exists()` instead of `if queryset:` or `if len(queryset):`
- Use `.count()` instead of `len(queryset.all())`
- Both perform database-level operations without loading objects
### Aggregation and Annotation
- `annotate()`: Add computed fields to each object (Count, Sum, Avg, etc.)
- `aggregate()`: Compute values across entire queryset
- Use `F()` expressions for database-level updates (`views=F('views') + 1`)
- Combine annotate with filter for "objects with at least N related items"
## Managers vs QuerySets
**Use QuerySets for chainable query logic.**
**Use Managers for model-level operations that don't return querysets.**
Manager: Think "factory methods" - `User.objects.create_user()`
QuerySet: Think "filters and transformations" - `Post.objects.published().recent()`
Most of the time, you want a custom QuerySet, not a custom Manager.
## Signals: Use Sparingly
Signals create implicit coupling and make code harder to follow. **Prefer explicit method calls.**
### When Signals Make Sense
- Audit logging (track all changes to a model)
- Cache invalidation (clear cache when model changes)
- Decoupling apps (third-party app needs to react to your models)
### When to Avoid Signals
- Business logic that should be in model methods
- Logic tightly coupled to the calling code (just call the function directly)
- Complex workflows (use explicit service layer instead)
**Rule of thumb**: If you control both the trigger and the reaction, don't use a signal.
## Migrations
### Workflow
- Run `makemigrations` after model changes
- Review generated migration files before applying
- Run `migrate` to apply migrations
- Migrations should be reversible when possible
### Data Migrations
Create with `makemigrations --empty app_name`. Use `apps.get_model()` to access models (not direct imports). Write both forward and reverse operations.
**Use data migrations for**: Populating new fields, transforming data, migrating between fields.
## Anti-Patterns to Avoid
### Query Anti-Patterns
- Iterating over objects and accessing relations without `select_related()`/`prefetch_related()`
- Using `if queryset:` instead of `.exists()`
- Using `len()` to count instead of `.count()`
- Loading entire objects when you only need specific fields
### Design Anti-Patterns
- Business logic in views instead of models
- Views performing calculations that belong in model methods
- Overusing signals for synchronous operations
- Creating new models when JSONField would suffice
- Forgetting to add indexes for filtered/sorted fields
## Integration
Works with:
- **pytest-django-patterns**: Factory-based model testing
- **celery-patterns**: Async operations on models (pass IDs, not instances)
- **django-forms**: ModelForm validation and saving