Skip to content

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

  1. Database Independence: The same code can switch between different databases
  2. Security: Automatically prevents SQL injection attacks
  3. Convenience: Use Python syntax to manipulate data with low learning cost
  4. Maintainability: Automatically generates migration files when models change

Django ORM Architecture

Python Code (Model) ←→ Django ORM ←→ Database

Defining 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

python
# 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.title

Model Class Components

  1. Field Definitions: Define columns in the database table
  2. Meta Class: Define model metadata
  3. Methods: Define model behavior and business logic

Field Types Explained

Django provides rich field types to map different data types.

Text Fields

python
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

python
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

python
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

python
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

python
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

python
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

python
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

python
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:

python
# 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 None

Using Models

python
# 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:

  1. ORM Advantages: Database independence, security, convenience
  2. Model Definition: Inherit Model class, define fields and methods
  3. Field Types: Rich types including text, number, date, boolean, file, etc.
  4. Field Options: Various options to control field behavior
  5. 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 →

Contents

Back to Course Outline

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).