Skip to main content
FootyCollect uses Django’s Multi-Table Inheritance (MTI) pattern to model different types of football memorabilia items. This architectural decision allows common attributes to be shared while supporting type-specific fields and behavior.

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.
All items share common attributes:
  • Name, description, photos
  • User ownership
  • Club, season, brand
  • Condition, privacy settings
  • Created/updated timestamps
But each type also has unique fields:
  • Jerseys have player names and numbers
  • Outerwear has type (hoodie, jacket, etc.)
  • Shorts have specific sizing

The Solution: Multi-Table Inheritance

MTI uses a BaseItem 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:
Source: footycollect/collection/models.py:180-312

Type-Specific Models

Each item type has its own model with a one-to-one relationship to BaseItem:

Jersey Model

Source: footycollect/collection/models.py:379-422

Shorts Model

Source: footycollect/collection/models.py:508-532

Outerwear Model

Source: footycollect/collection/models.py:535-565

Working with MTI Models

Creating Items

Creating an item involves creating both the BaseItem 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

Source: footycollect/collection/models.py:346-363

Optimizing Queries

Use select_related() to avoid N+1 queries:

Advantages of MTI

1. Clear Separation

Common and type-specific fields are clearly separated:
  • BaseItem contains 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:
  1. Create new model with OneToOneField to BaseItem
  2. Add choice to ITEM_TYPE_CHOICES
  3. Run migrations
  4. 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:
Mitigation: Use select_related() to make JOINs efficient.

Multiple Tables

Each item type creates a separate table:
  • collection_baseitem
  • collection_jersey
  • collection_shorts
  • collection_outerwear
  • collection_tracksuit
Benefit: Clear schema, no nullable type-specific fields, efficient storage.

Migration Complexity

Adding fields requires migrations on multiple tables:
Mitigation: Django migrations handle this automatically.

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:
Source: footycollect/collection/models.py:157-177 Usage:

Best Practices

Guidelines for working with MTI:
  1. Always use transactions when creating items (create BaseItem and specific type together)
  2. Use select_related() to optimize queries and avoid N+1 problems
  3. Implement save() methods on specific models to ensure item_type is set correctly
  4. Use get_specific_item() method when you need to access type-specific data dynamically
  5. Index common query patterns (user+type, club+season, etc.)
  6. Keep common fields in BaseItem - don’t duplicate in specific models
  7. Type-specific fields only in specific models - avoid nullable type fields in BaseItem

References