Building Django Models That Actually Make Sense

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.

Author

  • Mohammad Golam Dostogir, Software Engineer specializing in Python, Django, and AI solutions. Active contributor to open-source projects and tech communities, with experience delivering applications for global companies.
    GitHub

    View all posts
Index