Django 模型层 — 数据的基石
🕐 预计用时:2-3 小时 | 🎯 目标:掌握 Django ORM 的字段类型、关系定义、QuerySet 查询和数据库迁移
📖 今日目录
- 数据库迁移(makemigrations + migrate)
1. 定义模型
📋 模型 = 数据库表的 Python 类
# blog/models.py
from django.db import models
from django.contrib.auth.models import User # Django 自带的用户模型
class Category(models.Model):
name = models.CharField('分类名称', max_length=50, unique=True)
description = models.TextField('描述', blank=True, default='')
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['name']
def __str__(self):
return self.name
@property
def post_count(self):
return self.posts.count()
class Post(models.Model):
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
]
title = models.CharField('标题', max_length=200)
slug = models.SlugField('URL 别名', max_length=200, unique=True)
content = models.TextField('内容')
summary = models.CharField('摘要', max_length=300, blank=True)
status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default='draft')
views = models.PositiveIntegerField('浏览量', default=0)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts', verbose_name='作者')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='posts', verbose_name='分类')
tags = models.ManyToManyField('Tag', blank=True, related_name='posts', verbose_name='标签')
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['slug']),
]
def __str__(self):
return self.title
💡 Django 模型 vs SQLAlchemy 模型对比:
# SQLAlchemy(Flask)
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200))
# Django
class Post(models.Model):
title = models.CharField(max_length=200)
# id 自动创建!
Django 不需要手动定义 id 主键,它会自动添加 id = AutoField(primary_key=True)。
2. 字段类型
| | | |
|---|
CharField | | | CharField(max_length=100) |
TextField | | | TextField() |
IntegerField | | | IntegerField(default=0) |
FloatField | | | FloatField() |
DecimalField | | | DecimalField(max_digits=10, decimal_places=2) |
BooleanField | | | BooleanField(default=True) |
DateField | | | DateField(auto_now_add=True) |
DateTimeField | | | DateTimeField(auto_now=True) |
EmailField | | | EmailField() |
URLField | | | URLField() |
SlugField | | | SlugField(max_length=200) |
FileField | | | FileField(upload_to='uploads/') |
ImageField | | | ImageField(upload_to='images/') |
JSONField | | | JSONField(default=dict) |
🔧 字段常用参数
| | |
|---|
null | | CharField(null=True) |
blank | | CharField(blank=True) |
default | | IntegerField(default=0) |
unique | | CharField(unique=True) |
choices | | CharField(choices=STATUS) |
verbose_name | | CharField(verbose_name='用户名') |
db_index | | CharField(db_index=True) |
⚠️ null vs blank 的区别:
• null=True:数据库层面允许 NULL(空值存储)
• blank=True:表单验证层面允许空(不填也行)
通常一起用:CharField(null=True, blank=True)
但 CharField/TextField 不建议用 null=True(用 blank=True, default='' 更好)。
3. 字段选项
📅 auto_now vs auto_now_add
# auto_now_add=True:创建时自动设置,之后不再变
created_at = models.DateTimeField(auto_now_add=True)
# 第一次 save 时自动设为当前时间,之后修改不会变
# auto_now=True:每次 save 都更新
updated_at = models.DateTimeField(auto_now=True)
# 每次调用 save() 都更新为当前时间
📋 choices 选项
class Post(models.Model):
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
('archived', '已归档'),
]
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
# 使用
# post.status → 'draft'
# post.get_status_display() → '草稿'(显示中文)
4. 模型关系
🔗 一对多(ForeignKey)
# 一个用户有多篇文章
class Post(models.Model):
author = models.ForeignKey(
User, # 关联的模型
on_delete=models.CASCADE, # 删除作者时,文章也删除
related_name='posts', # 反向引用:user.posts.all()
)
# on_delete 选项:
# CASCADE → 级联删除(删作者,文章也删)
# PROTECT → 保护(有文章的作者不能删)
# SET_NULL → 设为 NULL(需要 null=True)
# SET_DEFAULT → 设为默认值
# DO_NOTHING → 什么都不做(不推荐)
# 使用
post = Post.objects.get(id=1)
post.author # 获取文章的作者(正向)
user.posts.all() # 获取用户的所有文章(反向)
🔗 多对多(ManyToManyField)
# 文章和标签:一篇文章多个标签,一个标签对应多篇文章
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Post(models.Model):
tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
# 使用
post.tags.add(tag1, tag2) # 添加标签
post.tags.remove(tag1) # 移除标签
post.tags.all() # 获取文章的所有标签
tag.posts.all() # 获取标签下的所有文章
# 创建时添加
post = Post.objects.create(title='Hello', author=user)
post.tags.set([tag1, tag2, tag3]) # 设置标签(覆盖)
🔗 一对一(OneToOneField)
# 用户和用户资料:一个用户只有一份资料
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
# 使用
user.profile.bio # 获取用户的个人简介
profile.user # 获取资料对应的用户
📊 关系对比
| | | |
|---|
| | | user.posts.all() |
| | | post.tags.all() |
| | | user.profile |
5. QuerySet 基础查询
📖 获取所有 / 获取单个
# 获取所有记录
all_posts = Post.objects.all() # SELECT * FROM posts
# 获取单个
post = Post.objects.get(id=1) # 获取 id=1 的文章(不存在会报错)
post = Post.objects.get(pk=1) # pk = primary_key(等价于 id=1)
# 安全获取(不存在返回 None)
post = Post.objects.filter(id=1).first()
# 获取或返回 404
from django.shortcuts import get_object_or_404
post = get_object_or_404(Post, pk=1) # 不存在返回 404 页面
🔍 过滤查询
# filter(返回 QuerySet,可能有多条)
published = Post.objects.filter(status='published')
# SELECT * FROM posts WHERE status = 'published'
recent = Post.objects.filter(views__gte=100)
# SELECT * FROM posts WHERE views >= 100
# exclude(排除符合条件的)
not_draft = Post.objects.exclude(status='draft')
# SELECT * FROM posts WHERE status != 'draft'
# 链式过滤
popular_published = Post.objects.filter(status='published').filter(views__gte=100)
# SELECT * FROM posts WHERE status='published' AND views >= 100
📋 字段查找(Field Lookups)
| | |
|---|
exact | = | filter(title__exact='Hello') |
iexact | = | filter(title__iexact='hello') |
contains | LIKE '%x%' | filter(title__contains='Django') |
icontains | LIKE '%x%' | filter(title__icontains='django') |
gt / gte | > / >= | filter(views__gte=100) |
lt / lte | < / <= | filter(views__lt=100) |
in | IN (...) | filter(status__in=['draft','published']) |
range | BETWEEN | filter(created_at__range=(start, end)) |
isnull | IS NULL | filter(category__isnull=True) |
startswith | LIKE 'x%' | filter(title__startswith='Django') |
year / month | | filter(created_at__year=2026) |
6. QuerySet 进阶查询
📊 排序与切片
# 排序
Post.objects.order_by('created_at') # 升序
Post.objects.order_by('-created_at') # 降序(- 表示降序)
Post.objects.order_by('-views', 'title') # 先按浏览量降序,再按标题升序
# 切片(对应 SQL 的 LIMIT + OFFSET)
Post.objects.all()[:5] # 前 5 条
Post.objects.all()[5:10] # 第 6-10 条
# 去重
Post.objects.values('category').distinct()
🔗 跨关系查询(JOIN)
# 查询作者名为 "alice" 的所有文章
Post.objects.filter(author__username='alice')
# SELECT * FROM posts INNER JOIN users ON posts.author_id = users.id
# WHERE users.username = 'alice'
# 查询分类名为 "Python" 的文章
Post.objects.filter(category__name='Python')
# 双下划线链式跨越
Post.objects.filter(author__profile__bio__contains='Python')
📋 Q 对象(复杂条件)
from django.db.models import Q
# OR 条件
Post.objects.filter(Q(views__gte=100) | Q(status='published'))
# WHERE views >= 100 OR status = 'published'
# NOT 条件
Post.objects.filter(~Q(status='draft'))
# WHERE status != 'draft'
# 复合条件
Post.objects.filter(
Q(status='published') & (Q(title__contains='Django') | Q(title__contains='Flask'))
)
# WHERE status='published' AND (title LIKE '%Django%' OR title LIKE '%Flask%')
7. 聚合与分组
from django.db.models import Count, Sum, Avg, Max, Min
# 聚合函数
Post.objects.aggregate(total_views=Sum('views'))
# {'total_views': 12345}
Post.objects.aggregate(avg_views=Avg('views'), max_views=Max('views'))
# {'avg_views': 123.5, 'max_views': 5000}
# 分组统计(annotate = GROUP BY)
from django.db.models import Count
# 每个分类的文章数量
Category.objects.annotate(post_count=Count('posts')).values('name', 'post_count')
# [{'name': 'Python', 'post_count': 15}, {'name': 'Flask', 'post_count': 8}]
# 每个作者的文章数,按数量降序
Post.objects.values('author__username').annotate(count=Count('id')).order_by('-count')
# 筛选分组结果(HAVING)
Category.objects.annotate(post_count=Count('posts')).filter(post_count__gte=5)
# 只返回文章数 >= 5 的分类
8. 数据库迁移
🔄 Django 迁移 vs Flask-Migrate
| | |
|---|
| python manage.py makemigrations | flask db migrate |
| python manage.py migrate | flask db upgrade |
| python manage.py migrate blog 0003 | flask db downgrade |
| python manage.py showmigrations | flask db history |
| python manage.py sqlmigrate blog 0001 | |
# 迁移四步法
# 1. 修改 models.py(添加字段、新建模型等)
# 2. 生成迁移文件
python manage.py makemigrations
# 输出:Migrations for 'blog':
# blog/migrations/0001_initial.py
# - Create model Post
# - Create model Category
# 3. 查看迁移会执行的 SQL
python manage.py sqlmigrate blog 0001
# 4. 执行迁移
python manage.py migrate
# 输出:Applying blog.0001_initial... OK
💡 Django 迁移的优势:
• 自动检测模型变化,生成迁移文件
• 迁移文件是 Python 代码,可以自定义数据迁移
• sqlmigrate 可以预览 SQL,安全第一
• 支持多应用独立迁移
• 迁移文件应该提交到 Git
9. Meta 选项
class Post(models.Model):
# ... 字段定义 ...
class Meta:
# 表名(默认:app名_类名小写,如 blog_post)
db_table = 'posts'
# 排序(默认排序)
ordering = ['-created_at']
# 唯一约束
unique_together = [['author', 'slug']] # 同一作者的 slug 不能重复
# 人类可读名称
verbose_name = '文章'
verbose_name_plural = '文章'
# 索引
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['author', 'status']),
]
10. 今日练习
🏋️ 练习 1:博客模型
# 完善 Day59 的博客项目模型:
# - Category:name, description
# - Post:title, content, summary, status, views, author(FK), category(FK), tags(M2M)
# - Tag:name
# - Comment:content, author(FK), post(FK), created_at
# 要求:定义好所有关系和 Meta
🏋️ 练习 2:QuerySet 查询
# 完成以下查询(用 Django shell 验证):
# 1. 获取所有已发布文章
# 2. 获取浏览量前 10 的文章
# 3. 搜索标题包含 "Django" 的文章
# 4. 获取某个分类下的所有文章
# 5. 统计每个分类的文章数量
# 6. 获取某个用户发表的所有评论
🏋️ 练习 3:数据库迁移
# 1. 为模型生成初始迁移
# 2. 用 sqlmigrate 查看 SQL
# 3. 执行迁移
# 4. 给 Post 添加 tags 字段(ManyToManyField)
# 5. 生成迁移并执行
# 6. 验证数据未丢失
11. 今日小结
| |
|---|
| models.Model + 字段类型 + Meta 选项 |
| CharField/TextField/IntegerField/DateTimeField/ForeignKey |
| null/blank/default/unique/choices/auto_now |
| ForeignKey(一对多)/ ManyToManyField(多对多)/ OneToOneField(一对一) |
| filter/exclude/order_by/slice/链式调用 |
| 双下划线 __:contains/gte/in/range/year/isnull |
| 复杂条件:OR(|) / NOT(~) / AND(&) |
| aggregate(Sum/Avg/Count) + annotate(分组) |
| makemigrations → migrate,安全修改表结构 |
🚀 明日预告:Day 61 — Django 视图层
模型层就绪了,明天深入视图层——函数视图(FBV)vs 类视图(CBV)、HttpRequest/HttpResponse、重定向、装饰器。让你的 Django 应用真正能响应用户的请求!