Django从零开始(二):模型与ORM数据库操作

一、ORM与模型简介

Django的ORM(Object-Relational Mapping,对象关系映射)是框架最强大的特性之一。它允许你用Python类来定义数据库表结构,用Python对象来操作数据库记录,而无需手写任何SQL语句。

ORM的优势

  • 数据库无关:同一套代码可以运行在SQLite、MySQL、PostgreSQL、Oracle上
  • 安全性:自动处理SQL注入防护
  • 开发效率:用Python语法代替SQL,代码更简洁易维护
  • 迁移管理:自动生成数据库变更脚本,版本化管理表结构

二、定义数据模型

blog/models.py 中定义模型类,每个模型类对应数据库中的一张表:

# blog/models.py

from django.db import models
from django.contrib.auth.models import User


class Category(models.Model):
    """文章分类模型"""
    name = models.CharField('分类名称', max_length=100, unique=True)
    slug = models.SlugField('URL别名', max_length=100, unique=True)
    description = models.TextField('分类描述', blank=True, default='')
    created_at = models.DateTimeField('创建时间', auto_now_add=True)
    updated_at = models.DateTimeField('更新时间', auto_now=True)

    class Meta:
        verbose_name = '分类'
        verbose_name_plural = '分类'
        ordering = ['name']

    def __str__(self):
        return self.name


class Tag(models.Model):
    """文章标签模型"""
    name = models.CharField('标签名称', max_length=50, unique=True)
    slug = models.SlugField('URL别名', max_length=50, unique=True)
    color = models.CharField('标签颜色', max_length=7, default='#333333')

    class Meta:
        verbose_name = '标签'
        verbose_name_plural = '标签'
        ordering = ['name']

    def __str__(self):
        return self.name


class Article(models.Model):
    """文章模型"""
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布'),
        ('archived', '已归档'),
    ]

    title = models.CharField('文章标题', max_length=200)
    slug = models.SlugField('URL别名', max_length=200, unique=True)
    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='标签')
    content = models.TextField('文章内容')
    summary = models.CharField('摘要', max_length=500, blank=True, default='')
    status = models.CharField(
        '发布状态',
        max_length=20,
        choices=STATUS_CHOICES,
        default='draft'
    )
    views = models.PositiveIntegerField('浏览次数', default=0)
    is_top = models.BooleanField('是否置顶', default=False)
    published_at = models.DateTimeField('发布时间', null=True, blank=True)
    created_at = models.DateTimeField('创建时间', auto_now_add=True)
    updated_at = models.DateTimeField('更新时间', auto_now=True)

    class Meta:
        verbose_name = '文章'
        verbose_name_plural = '文章'
        ordering = ['-is_top', '-published_at', '-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['status']),
            models.Index(fields=['slug']),
        ]

    def __str__(self):
        return self.title


class Comment(models.Model):
    """评论模型"""
    article = models.ForeignKey(
        Article,
        on_delete=models.CASCADE,
        related_name='comments',
        verbose_name='所属文章'
    )
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='replies',
        verbose_name='父评论'
    )
    nickname = models.CharField('昵称', max_length=50)
    email = models.EmailField('邮箱')
    content = models.TextField('评论内容')
    is_approved = models.BooleanField('是否审核通过', default=False)
    created_at = models.DateTimeField('创建时间', auto_now_add=True)

    class Meta:
        verbose_name = '评论'
        verbose_name_plural = '评论'
        ordering = ['created_at']

    def __str__(self):
        return f'{self.nickname} 评论了 {self.article.title}'

三、字段类型详解

3.1 常用字段类型

字段类型 数据库类型 说明
CharField VARCHAR 字符串,必须指定max_length
TextField TEXT 长文本,无长度限制
IntegerField INT 整数
BigIntegerField BIGINT 大整数
FloatField FLOAT 浮点数
DecimalField DECIMAL 精确小数,需指定max_digits和decimal_places
BooleanField BOOL 布尔值
DateField DATE 日期
DateTimeField DATETIME 日期时间
EmailField VARCHAR 邮箱,自动验证格式
URLField VARCHAR URL地址,自动验证
SlugField VARCHAR URL友好字符串
UUIDField CHAR(32) UUID标识符
FileField VARCHAR 文件上传
ImageField VARCHAR 图片上传(依赖Pillow)
JSONField JSON/TEXT JSON数据(Django 3.1+)

3.2 关系字段类型

# ForeignKey - 一对多关系
# 一个分类可以有多篇文章,文章只属于一个分类
category = models.ForeignKey(
    Category,
    on_delete=models.CASCADE,    # 级联删除:分类删除时,文章也删除
    # on_delete=models.SET_NULL,  # 设为NULL:分类删除时,文章的分类字段设为空
    # on_delete=models.PROTECT,   # 保护:有文章的分类不允许删除
    # on_delete=models.DO_NOTHING,# 什么都不做(不推荐)
    related_name='articles',     # 反向查询名称:category.articles
    null=True,
    blank=True,
    verbose_name='所属分类'
)

# ManyToManyField - 多对多关系
# 一篇文章可以有多个标签,一个标签可以属于多篇文章
tags = models.ManyToManyField(
    Tag,
    blank=True,
    related_name='articles',
    verbose_name='标签'
)

# OneToOneField - 一对一关系
# 例如:一个用户只有一个个人资料
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
    bio = models.TextField('个人简介', blank=True)

3.3 字段常用参数

# 通用参数
name = models.CharField(
    max_length=100,       # 最大长度
    verbose_name='名称',   # 人类可读名称(Admin显示)
    help_text='请输入名称', # 帮助文本(Admin表单提示)
    null=True,            # 数据库允许NULL
    blank=True,           # 表单允许空值
    default='',           # 默认值
    unique=True,          # 唯一约束
    db_index=True,        # 创建数据库索引
    editable=False,       # Admin中不可编辑
)

# 特殊参数
created = models.DateTimeField(auto_now_add=True)  # 创建时自动设置当前时间
updated = models.DateTimeField(auto_now=True)      # 每次保存时自动更新时间

四、数据库迁移操作

模型定义完成后,需要通过迁移操作将模型同步到数据库:

# 第一步:生成迁移文件
python manage.py makemigrations

# 输出示例:
# Migrations for 'blog':
#   blog/migrations/0001_initial.py
#     - Create model Category
#     - Create model Tag
#     - Create model Article
#     - Create model Comment
#     - Add field tags to article

# 查看生成的SQL语句(不执行)
python manage.py sqlmigrate blog 0001

# 第二步:执行迁移
python manage.py migrate

# 输出示例:
# Running migrations:
#   Applying blog.0001_initial... OK

# 查看迁移状态
python manage.py showmigrations blog

迁移回滚

# 回滚到指定迁移版本
python manage.py migrate blog 0002

# 回滚blog应用的所有迁移
python manage.py migrate blog zero

# 创建空的迁移文件(用于自定义数据迁移)
python manage.py makemigrations --empty blog

五、ORM增删改查操作(CRUD)

5.1 使用Django Shell

# 进入Django Shell(推荐使用ipython)
pip install ipython
python manage.py shell

# 导入模型
from blog.models import Category, Tag, Article, Comment
from django.contrib.auth.models import User

5.2 创建数据(Create)

# 方法1:直接创建并保存
category = Category.objects.create(
    name='Python',
    slug='python',
    description='Python编程相关文章'
)

# 方法2:先实例化再保存
tag = Tag(name='Django', slug='django', color='#092e20')
tag.save()

# 方法3:get_or_create - 获取或创建(避免重复)
category, created = Category.objects.get_or_create(
    slug='django',
    defaults={'name': 'Django', 'description': 'Django框架相关'}
)
# created为True表示新创建,False表示已存在

# 方法4:update_or_create - 更新或创建
category, created = Category.objects.update_or_create(
    slug='flask',
    defaults={'name': 'Flask', 'description': 'Flask框架'}
)

# 创建关联对象
user = User.objects.first()
article = Article.objects.create(
    title='Django ORM详解',
    slug='django-orm-guide',
    author=user,
    category=category,
    content='这是一篇关于Django ORM的文章...',
    status='published'
)

# 添加多对多关系
article.tags.add(tag)
article.tags.add(tag1, tag2)  # 批量添加
article.tags.set([tag1, tag2, tag3])  # 设置标签(覆盖之前的)
article.tags.remove(tag)  # 移除某个标签
article.tags.clear()  # 清空所有标签

5.3 查询数据(Read)

# 获取所有记录
all_categories = Category.objects.all()

# 获取单条记录
category = Category.objects.get(pk=1)
category = Category.objects.get(slug='python')

# get找不到会抛出 DoesNotExist 异常
try:
    category = Category.objects.get(slug='not-exist')
except Category.DoesNotExist:
    print('分类不存在')

# filter - 条件过滤(返回QuerySet)
published_articles = Article.objects.filter(status='published')
python_articles = Article.objects.filter(category__slug='python')

# exclude - 排除条件
draft_articles = Article.objects.exclude(status='published')

# 链式查询
results = Article.objects.filter(
    status='published'
).filter(
    category__name='Python'
).exclude(
    views=0
).order_by('-created_at')[:10]

# 常用查询条件
Article.objects.filter(title__exact='Django ORM')           # 精确匹配
Article.objects.filter(title__iexact='django orm')          # 不区分大小写精确匹配
Article.objects.filter(title__contains='Django')             # 包含
Article.objects.filter(title__icontains='django')            # 不区分大小写包含
Article.objects.filter(title__startswith='Django')           # 以...开头
Article.objects.filter(title__endswith='教程')               # 以...结尾
Article.objects.filter(views__gt=100)                        # 大于
Article.objects.filter(views__gte=100)                       # 大于等于
Article.objects.filter(views__lt=1000)                       # 小于
Article.objects.filter(views__lte=1000)                      # 小于等于
Article.objects.filter(views__range=[100, 500])              # 范围
Article.objects.filter(title__in=['标题1', '标题2'])         # 在列表中
Article.objects.filter(published_at__year=2026)              # 按年筛选
Article.objects.filter(published_at__month=4)                # 按月筛选
Article.objects.filter(published_at__date=date.today())      # 按日期筛选
Article.objects.filter(created_at__isnull=True)              # 为NULL
Article.objects.filter(tags__name='Django')                  # 跨关系查询

# 排序
Article.objects.order_by('created_at')      # 升序
Article.objects.order_by('-created_at')     # 降序
Article.objects.order_by('-views', 'title') # 多字段排序

5.4 更新数据(Update)

# 方法1:修改属性后保存
article = Article.objects.get(pk=1)
article.title = '新标题'
article.save()

# 方法2:批量更新
Article.objects.filter(status='draft').update(status='archived')

# 方法3:update_or_create
article, created = Article.objects.update_or_create(
    slug='django-orm-guide',
    defaults={
        'title': 'Django ORM完全指南',
        'content': '更新后的内容...',
    }
)

# 更新关联字段
article.category = new_category
article.save()

# 原子性更新(避免竞态条件)
from django.db.models import F
Article.objects.filter(pk=1).update(views=F('views') + 1)

5.5 删除数据(Delete)

# 删除单条记录
article = Article.objects.get(pk=1)
article.delete()

# 批量删除
Article.objects.filter(status='archived').delete()

# 注意:delete()会返回被删除的对象数量和每种类型的删除数量
result = Article.objects.filter(views=0).delete()
print(result)
# (15, {'blog.Article': 10, 'blog.Comment': 5})

六、高级查询技巧

6.1 聚合查询

from django.db.models import Count, Sum, Avg, Max, Min

# 统计文章总数
total = Article.objects.count()

# 每个分类的文章数
categories = Category.objects.annotate(
    article_count=Count('articles')
)
for cat in categories:
    print(f'{cat.name}: {cat.article_count}篇文章')

# 统计已发布文章的总浏览量
result = Article.objects.filter(
    status='published'
).aggregate(
    total_views=Sum('views'),
    avg_views=Avg('views'),
    max_views=Max('views'),
    min_views=Min('views'),
)
print(result)
# {'total_views': 50000, 'avg_views': 500, 'max_views': 5000, 'min_views': 10}

6.2 Q对象(复杂查询条件)

from django.db.models import Q

# OR条件
articles = Article.objects.filter(
    Q(title__contains='Django') | Q(title__contains='Flask')
)

# AND + NOT组合
articles = Article.objects.filter(
    Q(status='published') & ~Q(views=0)
)

# 复杂组合查询
articles = Article.objects.filter(
    (Q(title__contains='Django') & Q(status='published')) |
    (Q(title__contains='Flask') & Q(views__gt=100))
)

6.3 F对象(引用字段值)

from django.db.models import F

# 浏览量+1(直接在数据库层面操作,避免竞态条件)
Article.objects.filter(pk=1).update(views=F('views') + 1)

# 比较两个字段
Article.objects.filter(views__gt=F('comment_count'))

# F对象与算术运算
Article.objects.filter(views__gt=F('likes') * 10)

6.4 分页查询

from django.core.paginator import Paginator

# 创建分页器
article_list = Article.objects.filter(status='published')
paginator = Paginator(article_list, 10)  # 每页10条

# 获取指定页
page = paginator.page(1)
print(f'总共 {paginator.count} 篇文章')
print(f'总共 {paginator.num_pages} 页')
print(f'当前页有 {len(page)} 篇文章')
print(f'是否有下一页: {page.has_next()}')
print(f'下一页页码: {page.next_page_number()}')

# 遍历当前页数据
for article in page.object_list:
    print(article.title)

七、模型Meta选项

class Meta:
    # 排序规则
    ordering = ['-created_at']

    # 数据库表名(默认为 appname_modelname)
    db_table = 'blog_articles'

    # Admin显示名称
    verbose_name = '文章'
    verbose_name_plural = '文章列表'

    # 唯一约束(联合唯一)
    constraints = [
        models.UniqueConstraint(
            fields=['title', 'author'],
            name='unique_title_per_author'
        )
    ]

    # 索引
    indexes = [
        models.Index(fields=['-created_at'], name='idx_created_at'),
        models.Index(fields=['status', 'is_top'], name='idx_status_top'),
    ]

    # 抽象基类(不会创建数据库表)
    abstract = True

    # 权限
    permissions = [
        ('can_publish', '可以发布文章'),
        ('can_edit_all', '可以编辑所有文章'),
    ]

八、总结

本章我们深入学习了Django ORM的核心知识:

  • 定义模型类和字段类型(CharField、ForeignKey、ManyToManyField等)
  • 数据库迁移操作(makemigrations、migrate)
  • CRUD增删改查的多种方式
  • 高级查询:聚合、Q对象、F对象、分页
  • 模型Meta选项的常用配置

下一章我们将学习 Django视图(View)与URL路由,深入了解如何处理HTTP请求和返回响应。

发表回复

后才能评论