Skip to content

第4章:Django模型(Models)

4.1 Django 模型基础

ORM概念

ORM(Object-Relational Mapping,对象关系映射)是一种编程技术,用于在面向对象编程语言中实现不兼容类型系统之间的数据转换。Django ORM允许开发者使用Python代码来操作数据库,而不需要编写SQL语句。

ORM的优势

  1. 数据库无关性:同一套代码可以在不同数据库间切换
  2. 安全性:自动防止SQL注入攻击
  3. 便利性:使用Python语法操作数据,学习成本低
  4. 维护性:模型变更时自动生成迁移文件

Django ORM架构

Python代码 (Model) ←→ Django ORM ←→ 数据库

定义模型类

Django模型是继承自django.db.models.Model的Python类,每个模型对应数据库中的一张表。

基本模型示例

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):
    """文章分类模型"""
    name = models.CharField(max_length=100, verbose_name="分类名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="分类描述")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    
    class Meta:
        verbose_name = "分类"
        verbose_name_plural = "分类"
        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):
    """文章模型"""
    title = models.CharField(max_length=200, verbose_name="标题")
    content = models.TextField(verbose_name="内容")
    author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="分类")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
    
    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title

模型类的组成部分

  1. 字段定义:定义数据库表的列
  2. Meta类:定义模型的元数据
  3. 方法:定义模型的行为和业务逻辑

字段类型详解

Django提供了丰富的字段类型来映射不同的数据类型。

文本字段

python
class TextFieldExample(models.Model):
    # 短文本字段,必须指定max_length
    title = models.CharField(max_length=200, verbose_name="标题")
    
    # 长文本字段,用于大量文本
    content = models.TextField(verbose_name="内容")
    
    # 邮箱字段,自动验证邮箱格式
    email = models.EmailField(verbose_name="邮箱地址")
    
    # URL字段,自动验证URL格式
    website = models.URLField(blank=True, verbose_name="网站")
    
    # Slug字段,用于URL友好的字符串
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    
    # JSON字段(Django 3.1+)
    metadata = models.JSONField(default=dict, verbose_name="元数据")

数字字段

python
class NumberFieldExample(models.Model):
    # 整数字段
    view_count = models.IntegerField(default=0, verbose_name="浏览次数")
    
    # 正整数字段
    like_count = models.PositiveIntegerField(default=0, verbose_name="点赞数")
    
    # 小整数字段(-32768 到 32767)
    rating = models.SmallIntegerField(default=0, verbose_name="评分")
    
    # 大整数字段
    big_number = models.BigIntegerField(default=0, verbose_name="大数字")
    
    # 浮点数字段
    price = models.FloatField(default=0.0, verbose_name="价格")
    
    # 精确小数字段(推荐用于货币)
    precise_price = models.DecimalField(
        max_digits=10, 
        decimal_places=2, 
        default=0,
        verbose_name="精确价格"
    )

日期时间字段

python
class DateTimeFieldExample(models.Model):
    # 日期时间字段
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
    
    # 仅日期字段
    publish_date = models.DateField(default=timezone.now, verbose_name="发布日期")
    
    # 仅时间字段
    publish_time = models.TimeField(verbose_name="发布时间")
    
    # 时长字段
    duration = models.DurationField(verbose_name="时长")

布尔和选择字段

python
class BooleanAndChoiceExample(models.Model):
    # 布尔字段
    is_published = models.BooleanField(default=False, verbose_name="是否发布")
    
    # 可为空的布尔字段
    is_featured = models.BooleanField(null=True, blank=True, verbose_name="是否推荐")
    
    # 选择字段
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布'),
        ('archived', '已归档'),
    ]
    status = models.CharField(
        max_length=20, 
        choices=STATUS_CHOICES, 
        default='draft',
        verbose_name="状态"
    )
    
    PRIORITY_CHOICES = [
        (1, '低'),
        (2, '中'),
        (3, '高'),
    ]
    priority = models.IntegerField(
        choices=PRIORITY_CHOICES,
        default=1,
        verbose_name="优先级"
    )

文件字段

python
class FileFieldExample(models.Model):
    # 文件字段
    document = models.FileField(
        upload_to='documents/%Y/%m/',
        verbose_name="文档"
    )
    
    # 图片字段
    image = models.ImageField(
        upload_to='images/%Y/%m/',
        blank=True,
        verbose_name="图片"
    )
    
    # 自定义上传路径
    def user_directory_path(instance, filename):
        # 文件将上传到 MEDIA_ROOT/user_<id>/<filename>
        return f'user_{instance.user.id}/{filename}'
    
    avatar = models.ImageField(
        upload_to=user_directory_path,
        blank=True,
        verbose_name="头像"
    )

UUID字段

python
import uuid
from django.db import models

class UUIDFieldExample(models.Model):
    # UUID主键字段
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False
    )
    
    # 普通UUID字段
    tracking_id = models.UUIDField(
        default=uuid.uuid4,
        unique=True,
        verbose_name="跟踪ID"
    )

字段选项

每个字段都可以接受一系列选项来控制其行为。

通用字段选项

python
class FieldOptionsExample(models.Model):
    # null:数据库中是否允许为NULL
    description = models.TextField(null=True, verbose_name="描述")
    
    # blank:表单验证时是否允许为空
    optional_field = models.CharField(max_length=100, blank=True, verbose_name="可选字段")
    
    # default:默认值
    status = models.CharField(max_length=20, default='active', verbose_name="状态")
    
    # unique:是否唯一
    email = models.EmailField(unique=True, verbose_name="邮箱")
    
    # db_index:是否创建数据库索引
    slug = models.SlugField(db_index=True, verbose_name="别名")
    
    # verbose_name:字段的人类可读名称
    title = models.CharField(max_length=200, verbose_name="标题")
    
    # help_text:帮助文本
    password = models.CharField(
        max_length=128,
        help_text="密码长度至少8位",
        verbose_name="密码"
    )
    
    # editable:是否在表单中显示
    created_at = models.DateTimeField(
        auto_now_add=True,
        editable=False,
        verbose_name="创建时间"
    )

特殊字段选项

python
class SpecialFieldOptions(models.Model):
    # auto_now_add:创建时自动设置为当前时间
    created_at = models.DateTimeField(auto_now_add=True)
    
    # auto_now:每次保存时自动更新为当前时间
    updated_at = models.DateTimeField(auto_now=True)
    
    # upload_to:文件上传路径
    document = models.FileField(upload_to='documents/%Y/%m/%d/')
    
    # max_digits和decimal_places:小数字段选项
    price = models.DecimalField(max_digits=10, decimal_places=2)
    
    # choices:选择项
    CATEGORY_CHOICES = [
        ('tech', '技术'),
        ('life', '生活'),
        ('travel', '旅行'),
    ]
    category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)

实际应用示例

让我们创建一个完整的博客模型系统:

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):
    """抽象基类,提供时间戳字段"""
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
    
    class Meta:
        abstract = True

class Category(TimestampedModel):
    """文章分类模型"""
    name = models.CharField(max_length=100, unique=True, verbose_name="分类名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="分类描述")
    color = models.CharField(
        max_length=7,
        default='#007bff',
        help_text="十六进制颜色代码",
        verbose_name="颜色"
    )
    icon = models.CharField(max_length=50, blank=True, verbose_name="图标类名")
    is_active = models.BooleanField(default=True, verbose_name="是否启用")
    sort_order = models.PositiveIntegerField(default=0, verbose_name="排序")
    
    class Meta:
        verbose_name = "分类"
        verbose_name_plural = "分类"
        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):
        """获取分类下的文章数量"""
        return self.articles.filter(status='published').count()

class Tag(models.Model):
    """标签模型"""
    name = models.CharField(max_length=50, unique=True, verbose_name="标签名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="标签描述")
    
    class Meta:
        verbose_name = "标签"
        verbose_name_plural = "标签"
        ordering = ['name']
    
    def __str__(self):
        return self.name

class Article(TimestampedModel):
    """文章模型"""
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布'),
        ('archived', '已归档'),
    ]
    
    COMMENT_STATUS_CHOICES = [
        ('open', '开放评论'),
        ('closed', '关闭评论'),
        ('moderated', '审核后显示'),
    ]
    
    # 基本字段
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=200, verbose_name="标题")
    slug = models.SlugField(unique_for_date='publish_date', verbose_name="URL别名")
    content = models.TextField(verbose_name="内容")
    
    # 摘要字段
    excerpt = models.TextField(
        max_length=500,
        blank=True,
        help_text="如果为空,将自动从内容中提取",
        verbose_name="摘要"
    )
    
    # 关联字段
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='articles',
        verbose_name="作者"
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='articles',
        verbose_name="分类"
    )
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name='articles',
        verbose_name="标签"
    )
    
    # 状态字段
    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default='draft',
        verbose_name="状态"
    )
    comment_status = models.CharField(
        max_length=20,
        choices=COMMENT_STATUS_CHOICES,
        default='open',
        verbose_name="评论状态"
    )
    
    # 时间字段
    publish_date = models.DateTimeField(
        default=timezone.now,
        verbose_name="发布时间"
    )
    
    # 统计字段
    view_count = models.PositiveIntegerField(default=0, verbose_name="浏览次数")
    like_count = models.PositiveIntegerField(default=0, verbose_name="点赞数")
    
    # SEO字段
    meta_title = models.CharField(
        max_length=60,
        blank=True,
        help_text="SEO标题,留空则使用文章标题",
        verbose_name="SEO标题"
    )
    meta_description = models.CharField(
        max_length=160,
        blank=True,
        help_text="SEO描述",
        verbose_name="SEO描述"
    )
    meta_keywords = models.CharField(
        max_length=255,
        blank=True,
        help_text="SEO关键词,用逗号分隔",
        verbose_name="SEO关键词"
    )
    
    # 特色图片
    featured_image = models.ImageField(
        upload_to='articles/%Y/%m/',
        blank=True,
        verbose_name="特色图片"
    )
    
    # 其他选项
    is_featured = models.BooleanField(default=False, verbose_name="是否推荐")
    allow_comments = models.BooleanField(default=True, verbose_name="允许评论")
    is_top = models.BooleanField(default=False, verbose_name="是否置顶")
    
    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
        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):
        """重写保存方法,自动生成摘要"""
        if not self.excerpt and self.content:
            # 从内容中提取前200个字符作为摘要
            import re
            # 移除HTML标签
            clean_content = re.sub(r'<[^>]+>', '', self.content)
            self.excerpt = clean_content[:200] + '...' if len(clean_content) > 200 else clean_content
        
        # 自动生成SEO标题
        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):
        """检查文章是否已发布"""
        return (
            self.status == 'published' and 
            self.publish_date <= timezone.now()
        )
    
    @property
    def reading_time(self):
        """估算阅读时间(基于250字/分钟)"""
        word_count = len(self.content.split())
        return max(1, round(word_count / 250))
    
    def get_related_articles(self, limit=5):
        """获取相关文章"""
        related = Article.objects.filter(
            status='published',
            category=self.category
        ).exclude(id=self.id)
        
        if self.tags.exists():
            # 如果有标签,按标签相关性排序
            related = related.filter(tags__in=self.tags.all()).distinct()
        
        return related[:limit]

class Comment(TimestampedModel):
    """评论模型"""
    article = models.ForeignKey(
        Article,
        on_delete=models.CASCADE,
        related_name='comments',
        verbose_name="文章"
    )
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='comments',
        verbose_name="评论者"
    )
    content = models.TextField(verbose_name="评论内容")
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='replies',
        verbose_name="父评论"
    )
    is_approved = models.BooleanField(default=True, verbose_name="是否通过审核")
    
    class Meta:
        verbose_name = "评论"
        verbose_name_plural = "评论"
        ordering = ['created_at']
    
    def __str__(self):
        return f'{self.author.username} 对 "{self.article.title}" 的评论'
    
    @property
    def is_reply(self):
        """是否为回复评论"""
        return self.parent is not None

使用模型

python
# 在视图或shell中使用模型
from blog.models import Category, Article, Tag
from django.contrib.auth.models import User

# 创建分类
tech_category = Category.objects.create(
    name="技术",
    slug="tech",
    description="技术相关文章",
    color="#28a745"
)

# 创建标签
django_tag = Tag.objects.create(
    name="Django",
    slug="django"
)

# 创建文章
user = User.objects.get(username='admin')
article = Article.objects.create(
    title="Django入门教程",
    slug="django-tutorial",
    content="这是一篇Django入门教程...",
    author=user,
    category=tech_category,
    status='published'
)

# 添加标签
article.tags.add(django_tag)

# 查询示例
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)
)

小结

Django模型是应用的数据基础:

  1. ORM优势:数据库无关性、安全性、便利性
  2. 模型定义:继承Model类,定义字段和方法
  3. 字段类型:文本、数字、日期、布尔、文件等丰富类型
  4. 字段选项:控制字段行为的各种选项
  5. 最佳实践:抽象基类、自定义方法、合理的字段设计

关键要点:

  • 合理选择字段类型和选项
  • 使用抽象基类减少重复代码
  • 添加自定义方法增强模型功能
  • 考虑SEO和性能优化
  • 遵循Django命名约定

下一篇

我们将学习Django模型的关系字段,包括一对一、一对多和多对多关系。

4.2 Django 模型关系 →

目录

返回课程目录

Released under the Apache 2.0 License.