Models are where Django really shines. They’re not just database tables with Python syntax – they’re the heart of your application’s business logic. After building several e-commerce platforms, I’ve learned that good models make everything else easier, while bad models make your life miserable.
Today we’ll build the models for our bookstore application. I’ll show you the techniques I use to create models that are both powerful and maintainable.
Understanding Django Relationships in the Real World
The biggest mistake I see developers make is treating models like simple database tables. Django models should represent your business domain, not just store data.
Let’s start with our bookstore models and build them thoughtfully:
# inventory/models.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.urls import reverse
from django.utils import timezone
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, help_text="Brief description of this category")
slug = models.SlugField(unique=True, help_text="URL-friendly version of the name")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = "categories"
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('inventory:category-detail', kwargs={'slug': self.slug})
class Author(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
bio = models.TextField(blank=True)
birth_date = models.DateField(null=True, blank=True)
website = models.URLField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['last_name', 'first_name']
indexes = [
models.Index(fields=['last_name', 'first_name']),
]
def __str__(self):
return f"{self.first_name} {self.last_name}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
def get_absolute_url(self):
return reverse('inventory:author-detail', kwargs={'pk': self.pk})
The Main Book Model with Business Logic
Here’s where things get interesting. The Book model isn’t just storing data – it’s encapsulating business rules:
class BookManager(models.Manager):
def available(self):
"""Return only books that are available for purchase"""
return self.filter(is_available=True, stock_quantity__gt=0)
def by_category(self, category_slug):
"""Filter books by category slug"""
return self.filter(category__slug=category_slug, is_available=True)
def bestsellers(self, limit=10):
"""Return top selling books (this would use actual sales data in real app)"""
return self.filter(is_available=True).order_by('-publication_date')[:limit]
class Book(models.Model):
# Basic information
title = models.CharField(max_length=200)
subtitle = models.CharField(max_length=200, blank=True)
authors = models.ManyToManyField(Author, related_name='books')
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='books')
# Publishing details
isbn_10 = models.CharField(max_length=10, blank=True, unique=True, null=True)
isbn_13 = models.CharField(max_length=13, unique=True)
publisher = models.CharField(max_length=200)
publication_date = models.DateField()
pages = models.PositiveIntegerField(validators=[MinValueValidator(1)])
# Pricing and inventory
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0.01)]
)
cost_price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0.01)],
help_text="What we paid for this book"
)
stock_quantity = models.PositiveIntegerField(default=0)
# Status and metadata
is_available = models.BooleanField(default=True)
featured = models.BooleanField(default=False, help_text="Show on homepage")
description = models.TextField()
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Custom manager
objects = BookManager()
class Meta:
ordering = ['-created_at', 'title']
indexes = [
models.Index(fields=['isbn_13']),
models.Index(fields=['publication_date']),
models.Index(fields=['category', 'is_available']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('inventory:book-detail', kwargs={'pk': self.pk})
@property
def is_in_stock(self):
"""Check if book is actually available for purchase"""
return self.stock_quantity > 0 and self.is_available
@property
def profit_margin(self):
"""Calculate profit margin percentage"""
if self.cost_price > 0:
return ((self.price - self.cost_price) / self.cost_price) * 100
return 0
def reduce_stock(self, quantity=1):
"""Reduce stock when a book is sold"""
if self.stock_quantity >= quantity:
self.stock_quantity -= quantity
if self.stock_quantity == 0:
self.is_available = False
self.save()
return True
return False
def add_stock(self, quantity):
"""Add stock when inventory is restocked"""
self.stock_quantity += quantity
if quantity > 0:
self.is_available = True
self.save()
Review System for Social Proof
Every good bookstore needs reviews. Here’s how I typically implement them:
class BookReview(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='reviews')
reviewer_name = models.CharField(max_length=100)
reviewer_email = models.EmailField()
rating = models.PositiveIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
title = models.CharField(max_length=200)
review_text = models.TextField()
is_approved = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
unique_together = ['book', 'reviewer_email']
def __str__(self):
return f"{self.title} - {self.rating}/5 stars"
Adding Business Logic to Your Models
This is where most tutorials stop, but real applications need more than basic CRUD. Let’s add some methods that make our models actually useful:
# Add these methods to the Book model
def get_average_rating(self):
"""Calculate average rating from approved reviews"""
approved_reviews = self.reviews.filter(is_approved=True)
if approved_reviews.exists():
total_rating = sum(review.rating for review in approved_reviews)
return round(total_rating / approved_reviews.count(), 1)
return 0
def get_rating_distribution(self):
"""Get distribution of ratings for display"""
distribution = {i: 0 for i in range(1, 6)}
for review in self.reviews.filter(is_approved=True):
distribution[review.rating] += 1
return distribution
def similar_books(self, limit=4):
"""Find books in the same category by different authors"""
return Book.objects.filter(
category=self.category
).exclude(
pk=self.pk
).exclude(
authors__in=self.authors.all()
)[:limit]
Creating and Running Migrations
Now let’s create our database tables. Django’s migration system is one of its best features:
# Create migration files
python manage.py makemigrations inventory
# Apply migrations to database
python manage.py migrate
# Create a superuser to access admin
python manage.py createsuperuser
Populating with Sample Data
Let’s create some sample data to test our models. Create a management command:
# inventory/management/commands/populate_books.py
from django.core.management.base import BaseCommand
from inventory.models import Category, Author, Book
from datetime import date
class Command(BaseCommand):
help = 'Populate database with sample books'
def handle(self, *args, **options):
# Create categories
fiction = Category.objects.get_or_create(
name='Fiction',
slug='fiction',
description='Novels and short stories'
)[0]
non_fiction = Category.objects.get_or_create(
name='Non-Fiction',
slug='non-fiction',
description='Educational and informational books'
)[0]
# Create authors
author1 = Author.objects.get_or_create(
first_name='Jane',
last_name='Doe',
bio='Bestselling fiction author'
)[0]
# Create books
book1 = Book.objects.get_or_create(
title='The Great Adventure',
isbn_13='9781234567890',
category=fiction,
price=24.99,
cost_price=15.00,
stock_quantity=50,
publication_date=date(2022, 1, 15),
pages=320,
publisher='Great Books Publishing',
description='An amazing adventure story...'
)[0]
book1.authors.add(author1)
self.stdout.write(
self.style.SUCCESS('Successfully populated database')
)
Run it with:
python manage.py populate_books
Testing Your Models
Good models deserve good tests. Here’s how I test the business logic:
# inventory/tests.py
from django.test import TestCase
from django.core.exceptions import ValidationError
from .models import Category, Author, Book
from datetime import date
class BookModelTest(TestCase):
def setUp(self):
self.category = Category.objects.create(
name='Test Category',
slug='test-category'
)
self.author = Author.objects.create(
first_name='Test',
last_name='Author'
)
self.book = Book.objects.create(
title='Test Book',
isbn_13='9781234567890',
category=self.category,
price=19.99,
cost_price=12.00,
stock_quantity=10,
publication_date=date.today(),
pages=200,
publisher='Test Publisher',
description='A test book'
)
self.book.authors.add(self.author)
def test_book_string_representation(self):
self.assertEqual(str(self.book), 'Test Book')
def test_is_in_stock_property(self):
self.assertTrue(self.book.is_in_stock)
self.book.stock_quantity = 0
self.book.save()
self.assertFalse(self.book.is_in_stock)
def test_reduce_stock_method(self):
original_stock = self.book.stock_quantity
result = self.book.reduce_stock(3)
self.assertTrue(result)
self.assertEqual(self.book.stock_quantity, original_stock - 3)
def test_profit_margin_calculation(self):
expected_margin = ((19.99 - 12.00) / 12.00) * 100
self.assertAlmostEqual(self.book.profit_margin, expected_margin, places=2)
Run tests with:
python manage.py test inventory
Key Takeaways for Better Models
The models we built today go beyond simple data storage. They include:
- Business logic – Methods that encapsulate domain rules
- Custom managers – Reusable query methods
- Properties – Calculated fields that don’t need database storage
- Validation – Ensuring data integrity at the model level
- Relationships – Proper foreign keys and many-to-many fields
In the next article, we’ll build views that use these models effectively. We’ll cover both function-based and class-based views, and I’ll show you the patterns I use to keep views clean and maintainable.
Remember: models are the foundation of your Django application. Spend time getting them right, and everything else becomes much easier.