The Challenge
FootyCollect needs to support multiple types of football memorabilia:- Jerseys - with player names, numbers, sleeve type
- Shorts - with sizes and numbers
- Outerwear - jackets, hoodies, windbreakers
- Tracksuits - training wear
- Other items - pins, hats, socks, etc.
- Name, description, photos
- User ownership
- Club, season, brand
- Condition, privacy settings
- Created/updated timestamps
- Jerseys have player names and numbers
- Outerwear has type (hoodie, jacket, etc.)
- Shorts have specific sizing
The Solution: Multi-Table Inheritance
MTI uses aBaseItem model for common fields and separate models for each item type, connected via one-to-one relationships.
This architectural decision is documented in ADR 0001: Multi-Table Inheritance for Item Types.
Implementation
BaseItem Model
The base model contains all common fields:footycollect/collection/models.py:180-312
Type-Specific Models
Each item type has its own model with a one-to-one relationship toBaseItem:
Jersey Model
footycollect/collection/models.py:379-422
Shorts Model
footycollect/collection/models.py:508-532
Outerwear Model
footycollect/collection/models.py:535-565
Working with MTI Models
Creating Items
Creating an item involves creating both theBaseItem and the specific type:
The
save() method on specific models automatically sets the correct item_type on the base item, so you can often omit it when creating the base item.Querying Items
Get All Items
Get Specific Type
Access Type-Specific Data from BaseItem
Get Specific Item Dynamically
footycollect/collection/models.py:346-363
Optimizing Queries
Useselect_related() to avoid N+1 queries:
Advantages of MTI
1. Clear Separation
Common and type-specific fields are clearly separated:BaseItemcontains what all items share- Specific models contain only type-specific fields
- No nullable fields for type-specific attributes
2. Type Safety
Each item type has its own model with proper field types:3. Query Flexibility
Can query all items or specific types:4. Extensibility
Easy to add new item types without modifying existing code:- Create new model with
OneToOneFieldtoBaseItem - Add choice to
ITEM_TYPE_CHOICES - Run migrations
- Existing code continues to work
5. Django ORM Support
Works seamlessly with Django’s ORM and admin:- Automatic admin interfaces
- Model forms work correctly
- Migrations are straightforward
- Signals work as expected
Trade-offs and Considerations
Database JOINs
Accessing type-specific fields requires JOINs:select_related() to make JOINs efficient.
Multiple Tables
Each item type creates a separate table:collection_baseitemcollection_jerseycollection_shortscollection_outerwearcollection_tracksuit
Migration Complexity
Adding fields requires migrations on multiple tables:Alternative Patterns Considered
Single Table Inheritance (STI)
Rejected because:- Requires nullable fields for type-specific attributes
- Wastes storage space
- Less type-safe
- Harder to maintain as types grow
Abstract Base Classes
Rejected because:- Can’t query all items together
- Can’t have ForeignKeys to “any item”
- No shared table for common attributes
Generic Foreign Keys
Rejected because:- Loses referential integrity
- Harder to query
- More complex code
- No database-level constraints
Custom Managers for MTI
FootyCollect provides custom managers for common queries:footycollect/collection/models.py:157-177
Usage:
Best Practices
Guidelines for working with MTI:
- Always use transactions when creating items (create BaseItem and specific type together)
- Use select_related() to optimize queries and avoid N+1 problems
- Implement save() methods on specific models to ensure item_type is set correctly
- Use get_specific_item() method when you need to access type-specific data dynamically
- Index common query patterns (user+type, club+season, etc.)
- Keep common fields in BaseItem - don’t duplicate in specific models
- Type-specific fields only in specific models - avoid nullable type fields in BaseItem