| 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. |
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
- Use
TextChoices/IntegerChoicesfor status fields and enums - Add
get_absolute_url()for canonical object URLs - Include
__str__()for readable representations - Set proper
orderingin Meta for consistent default sorting - Add database indexes for frequently filtered/sorted fields
- Use abstract base models for shared fields (timestamps, soft deletes, etc.)
- Use
blank=True, default=""for optional text fields (avoid null) - Use
null=True, blank=Truefor optional foreign keys - For unique optional fields, use
null=Trueto avoid collision issues - Leverage
JSONFieldfor flexible metadata (avoid creating many optional fields) - Set appropriate
max_lengthbased on actual data needs
- 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_fieldswhen saving partial changes
Custom QuerySet classes are your secret weapon. They make queries reusable, chainable, and testable.
Define a QuerySet subclass with domain-specific filter methods
Attach it to your model: objects = YourQuerySet.as_manager()
Chain methods for composable queries
- 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
- Filtering by status/state
- Date range queries (recent, upcoming, expired)
- User-scoped queries (owned_by, visible_to)
- Combined lookups (published_and_recent)
- select_related(): Use for ForeignKey and OneToOneField (creates SQL JOIN)
- prefetch_related(): Use for ManyToManyField and reverse ForeignKeys (separate query + Python join)
- only(): Load specific fields when you don't need the whole object
- defer(): Exclude heavy fields (TextField, JSONField) you won't use
- Prefetch() object: Customize prefetch with filters and select_related
- Use
.exists()instead ofif queryset:orif len(queryset): - Use
.count()instead oflen(queryset.all()) - Both perform database-level operations without loading objects
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"
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 create implicit coupling and make code harder to follow. Prefer explicit method calls.
- 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)
- 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.
- Run
makemigrationsafter model changes - Review generated migration files before applying
- Run
migrateto apply migrations - Migrations should be reversible when possible
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.
- 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
- 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
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