Chapter 4: Django Models
4.1 Django Model Basics
ORM Concept
ORM (Object-Relational Mapping) is a programming technique for converting data between incompatible type systems in object-oriented programming languages. Django ORM allows developers to manipulate databases using Python code without writing SQL statements.
ORM Advantages
- Database Independence: The same code can switch between different databases
- Security: Automatically prevents SQL injection attacks
- Convenience: Use Python syntax to manipulate data with low learning cost
- Maintainability: Automatically generates migration files when models change
Django ORM Architecture
Python Code (Model) ←→ Django ORM ←→ DatabaseDefining Model Classes
Django models are Python classes that inherit from django.db.models.Model. Each model corresponds to a table in the database.
Basic Model Example
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.urls import reverse
class Category(models.Model):
"""Article category model"""
name = models.CharField(max_length=100, verbose_name="Category Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Category Description")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'slug': self.slug})
class Article(models.Model):
"""Article model"""
title = models.CharField(max_length=200, verbose_name="Title")
content = models.TextField(verbose_name="Content")
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Author")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="Category")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated Time")
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
ordering = ['-created_at']
def __str__(self):
return self.titleModel Class Components
- Field Definitions: Define columns in the database table
- Meta Class: Define model metadata
- Methods: Define model behavior and business logic
Field Types Explained
Django provides rich field types to map different data types.
Text Fields
class TextFieldExample(models.Model):
# Short text field, must specify max_length
title = models.CharField(max_length=200, verbose_name="Title")
# Long text field for large amounts of text
content = models.TextField(verbose_name="Content")
# Email field, automatically validates email format
email = models.EmailField(verbose_name="Email Address")
# URL field, automatically validates URL format
website = models.URLField(blank=True, verbose_name="Website")
# Slug field for URL-friendly strings
slug = models.SlugField(unique=True, verbose_name="URL Alias")
# JSON field (Django 3.1+)
metadata = models.JSONField(default=dict, verbose_name="Metadata")Number Fields
class NumberFieldExample(models.Model):
# Integer field
view_count = models.IntegerField(default=0, verbose_name="View Count")
# Positive integer field
like_count = models.PositiveIntegerField(default=0, verbose_name="Like Count")
# Small integer field (-32768 to 32767)
rating = models.SmallIntegerField(default=0, verbose_name="Rating")
# Big integer field
big_number = models.BigIntegerField(default=0, verbose_name="Big Number")
# Float field
price = models.FloatField(default=0.0, verbose_name="Price")
# Precise decimal field (recommended for currency)
precise_price = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
verbose_name="Precise Price"
)Date and Time Fields
class DateTimeFieldExample(models.Model):
# Date time field
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated Time")
# Date only field
publish_date = models.DateField(default=timezone.now, verbose_name="Publish Date")
# Time only field
publish_time = models.TimeField(verbose_name="Publish Time")
# Duration field
duration = models.DurationField(verbose_name="Duration")Boolean and Choice Fields
class BooleanAndChoiceExample(models.Model):
# Boolean field
is_published = models.BooleanField(default=False, verbose_name="Is Published")
# Nullable boolean field
is_featured = models.BooleanField(null=True, blank=True, verbose_name="Is Featured")
# Choice field
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
verbose_name="Status"
)
PRIORITY_CHOICES = [
(1, 'Low'),
(2, 'Medium'),
(3, 'High'),
]
priority = models.IntegerField(
choices=PRIORITY_CHOICES,
default=1,
verbose_name="Priority"
)File Fields
class FileFieldExample(models.Model):
# File field
document = models.FileField(
upload_to='documents/%Y/%m/',
verbose_name="Document"
)
# Image field
image = models.ImageField(
upload_to='images/%Y/%m/',
blank=True,
verbose_name="Image"
)
# Custom upload path
def user_directory_path(instance, filename):
# File will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return f'user_{instance.user.id}/{filename}'
avatar = models.ImageField(
upload_to=user_directory_path,
blank=True,
verbose_name="Avatar"
)UUID Fields
import uuid
from django.db import models
class UUIDFieldExample(models.Model):
# UUID primary key field
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
# Regular UUID field
tracking_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
verbose_name="Tracking ID"
)Field Options
Each field can accept a series of options to control its behavior.
Common Field Options
class FieldOptionsExample(models.Model):
# null: Whether NULL is allowed in the database
description = models.TextField(null=True, verbose_name="Description")
# blank: Whether empty is allowed during form validation
optional_field = models.CharField(max_length=100, blank=True, verbose_name="Optional Field")
# default: Default value
status = models.CharField(max_length=20, default='active', verbose_name="Status")
# unique: Whether it's unique
email = models.EmailField(unique=True, verbose_name="Email")
# db_index: Whether to create a database index
slug = models.SlugField(db_index=True, verbose_name="Alias")
# verbose_name: Human-readable name of the field
title = models.CharField(max_length=200, verbose_name="Title")
# help_text: Help text
password = models.CharField(
max_length=128,
help_text="Password must be at least 8 characters",
verbose_name="Password"
)
# editable: Whether to display in forms
created_at = models.DateTimeField(
auto_now_add=True,
editable=False,
verbose_name="Created Time"
)Special Field Options
class SpecialFieldOptions(models.Model):
# auto_now_add: Automatically set to current time when created
created_at = models.DateTimeField(auto_now_add=True)
# auto_now: Automatically updated to current time on each save
updated_at = models.DateTimeField(auto_now=True)
# upload_to: File upload path
document = models.FileField(upload_to='documents/%Y/%m/%d/')
# max_digits and decimal_places: Decimal field options
price = models.DecimalField(max_digits=10, decimal_places=2)
# choices: Selection options
CATEGORY_CHOICES = [
('tech', 'Technology'),
('life', 'Life'),
('travel', 'Travel'),
]
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)Practical Examples
Let's create a complete blog model system:
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.urls import reverse
import uuid
class TimestampedModel(models.Model):
"""Abstract base class that provides timestamp fields"""
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created Time")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated Time")
class Meta:
abstract = True
class Category(TimestampedModel):
"""Article category model"""
name = models.CharField(max_length=100, unique=True, verbose_name="Category Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Category Description")
color = models.CharField(
max_length=7,
default='#007bff',
help_text="Hex color code",
verbose_name="Color"
)
icon = models.CharField(max_length=50, blank=True, verbose_name="Icon Class Name")
is_active = models.BooleanField(default=True, verbose_name="Is Active")
sort_order = models.PositiveIntegerField(default=0, verbose_name="Sort Order")
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
ordering = ['sort_order', 'name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'slug': self.slug})
@property
def article_count(self):
"""Get the number of articles in the category"""
return self.articles.filter(status='published').count()
class Tag(models.Model):
"""Tag model"""
name = models.CharField(max_length=50, unique=True, verbose_name="Tag Name")
slug = models.SlugField(unique=True, verbose_name="URL Alias")
description = models.TextField(blank=True, verbose_name="Tag Description")
class Meta:
verbose_name = "Tag"
verbose_name_plural = "Tags"
ordering = ['name']
def __str__(self):
return self.name
class Article(TimestampedModel):
"""Article model"""
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
COMMENT_STATUS_CHOICES = [
('open', 'Open Comments'),
('closed', 'Closed Comments'),
('moderated', 'Show After Moderation'),
]
# Basic fields
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200, verbose_name="Title")
slug = models.SlugField(unique_for_date='publish_date', verbose_name="URL Alias")
content = models.TextField(verbose_name="Content")
# Excerpt field
excerpt = models.TextField(
max_length=500,
blank=True,
help_text="If empty, will be automatically extracted from content",
verbose_name="Excerpt"
)
# Relationship fields
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='articles',
verbose_name="Author"
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='articles',
verbose_name="Category"
)
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='articles',
verbose_name="Tags"
)
# Status fields
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
verbose_name="Status"
)
comment_status = models.CharField(
max_length=20,
choices=COMMENT_STATUS_CHOICES,
default='open',
verbose_name="Comment Status"
)
# Time fields
publish_date = models.DateTimeField(
default=timezone.now,
verbose_name="Publish Date"
)
# Statistics fields
view_count = models.PositiveIntegerField(default=0, verbose_name="View Count")
like_count = models.PositiveIntegerField(default=0, verbose_name="Like Count")
# SEO fields
meta_title = models.CharField(
max_length=60,
blank=True,
help_text="SEO title, leave empty to use article title",
verbose_name="SEO Title"
)
meta_description = models.CharField(
max_length=160,
blank=True,
help_text="SEO description",
verbose_name="SEO Description"
)
meta_keywords = models.CharField(
max_length=255,
blank=True,
help_text="SEO keywords, separated by commas",
verbose_name="SEO Keywords"
)
# Featured image
featured_image = models.ImageField(
upload_to='articles/%Y/%m/',
blank=True,
verbose_name="Featured Image"
)
# Other options
is_featured = models.BooleanField(default=False, verbose_name="Is Featured")
allow_comments = models.BooleanField(default=True, verbose_name="Allow Comments")
is_top = models.BooleanField(default=False, verbose_name="Is Top")
class Meta:
verbose_name = "Article"
verbose_name_plural = "Articles"
ordering = ['-is_top', '-publish_date']
indexes = [
models.Index(fields=['status', 'publish_date']),
models.Index(fields=['author', 'status']),
models.Index(fields=['category', 'status']),
]
def __str__(self):
return self.title
def save(self, *args, **kwargs):
"""Override save method to automatically generate excerpt"""
if not self.excerpt and self.content:
# Extract first 200 characters from content as excerpt
import re
# Remove HTML tags
clean_content = re.sub(r'<[^>]+>', '', self.content)
self.excerpt = clean_content[:200] + '...' if len(clean_content) > 200 else clean_content
# Automatically generate SEO title
if not self.meta_title:
self.meta_title = self.title[:60]
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('blog:article_detail', kwargs={
'year': self.publish_date.year,
'month': self.publish_date.month,
'day': self.publish_date.day,
'slug': self.slug
})
@property
def is_published(self):
"""Check if the article is published"""
return (
self.status == 'published' and
self.publish_date <= timezone.now()
)
@property
def reading_time(self):
"""Estimate reading time (based on 250 words/minute)"""
word_count = len(self.content.split())
return max(1, round(word_count / 250))
def get_related_articles(self, limit=5):
"""Get related articles"""
related = Article.objects.filter(
status='published',
category=self.category
).exclude(id=self.id)
if self.tags.exists():
# If there are tags, sort by tag relevance
related = related.filter(tags__in=self.tags.all()).distinct()
return related[:limit]
class Comment(TimestampedModel):
"""Comment model"""
article = models.ForeignKey(
Article,
on_delete=models.CASCADE,
related_name='comments',
verbose_name="Article"
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='comments',
verbose_name="Commenter"
)
content = models.TextField(verbose_name="Comment Content")
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='replies',
verbose_name="Parent Comment"
)
is_approved = models.BooleanField(default=True, verbose_name="Is Approved")
class Meta:
verbose_name = "Comment"
verbose_name_plural = "Comments"
ordering = ['created_at']
def __str__(self):
return f'{self.author.username}\'s comment on "{self.article.title}"'
@property
def is_reply(self):
"""Whether it's a reply comment"""
return self.parent is not NoneUsing Models
# Use models in views or shell
from blog.models import Category, Article, Tag
from django.contrib.auth.models import User
# Create category
tech_category = Category.objects.create(
name="Technology",
slug="tech",
description="Technology related articles",
color="#28a745"
)
# Create tag
django_tag = Tag.objects.create(
name="Django",
slug="django"
)
# Create article
user = User.objects.get(username='admin')
article = Article.objects.create(
title="Django Tutorial",
slug="django-tutorial",
content="This is a Django tutorial...",
author=user,
category=tech_category,
status='published'
)
# Add tag
article.tags.add(django_tag)
# Query examples
published_articles = Article.objects.filter(status='published')
tech_articles = Article.objects.filter(category__slug='tech')
recent_articles = Article.objects.filter(
status='published',
publish_date__gte=timezone.now() - timezone.timedelta(days=7)
)Summary
Django models are the data foundation of applications:
- ✅ ORM Advantages: Database independence, security, convenience
- ✅ Model Definition: Inherit Model class, define fields and methods
- ✅ Field Types: Rich types including text, number, date, boolean, file, etc.
- ✅ Field Options: Various options to control field behavior
- ✅ Best Practices: Abstract base classes, custom methods, reasonable field design
Key Points:
- Choose field types and options appropriately
- Use abstract base classes to reduce duplicate code
- Add custom methods to enhance model functionality
- Consider SEO and performance optimization
- Follow Django naming conventions
Next
We will learn about Django model relationship fields, including one-to-one, one-to-many, and many-to-many relationships.
4.2 Django Model Relationships →