← 返回Web开发目录
← 返回学习笔记首页
专题: Python Web开发系统学习
关键词: Python, Web开发, Django ORM, 模型, 数据库迁移, QuerySet, ForeignKey, 聚合查询, 事务
一、模型定义基础
Django的ORM(对象关系映射)是其最强大的核心功能之一,它允许开发者用Python类来定义数据库表结构,无需直接编写SQL语句。每个模型类对应一张数据库表,每个类属性对应表中的一个字段。Django通过这种方式实现了数据库的抽象化,使开发者能够专注于业务逻辑而非底层SQL细节。
1.1 模型类的定义
所有Django模型都必须继承 django.db.models.Model 类。每个模型类对应一张数据库表,Django会自动为每个模型添加一个自增主键字段(id),除非你显式定义其他主键。
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
view_count = models.IntegerField(default=0)
is_published = models.BooleanField(default=False)
publish_date = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
1.2 常用字段类型
Django提供了丰富的字段类型来满足不同的数据存储需求。字符串类字段包括 CharField(定长字符串,必须指定 max_length)、TextField(大文本)、EmailField(自动验证邮箱格式)和 URLField(自动验证URL格式)。数字类字段有 IntegerField、FloatField 和 DecimalField。时间日期类字段包括 DateField、DateTimeField 和 TimeField。布尔类 BooleanField 用于真/假值。文件类字段有 FileField(文件上传)和 ImageField(图片上传,需 Pillow 库)。此外还有 JSONField(存储JSON数据,Django 3.1+)等高级字段。
1.3 字段选项详解
每个字段类型都可以接受一组通用的字段选项来控制其行为。max_length 用于 CharField 等字符字段,限制最大字符数。null 决定数据库层面是否允许为空,blank 决定表单层面是否允许为空(两者有本质区别)。default 设置默认值。unique 确保字段值在表中唯一。choices 提供下拉选择项的限定范围,通常传入一个二元组列表。verbose_name 设置字段的人类可读名称。db_index 为字段创建数据库索引以加速查询。
class Category(models.Model):
STATUS_CHOICES = [
('active', '启用'),
('inactive', '禁用'),
]
name = models.CharField(
max_length=100,
unique=True,
verbose_name='分类名称',
db_index=True
)
sort_order = models.IntegerField(default=0, verbose_name='排序')
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default='active'
)
class Meta:
db_table = 'blog_category'
ordering = ['sort_order', 'name']
verbose_name = '文章分类'
verbose_name_plural = '文章分类'
unique_together = [('name', 'status')]
1.4 Meta内部类
Meta 内部类用于定义模型的元数据。db_table 指定数据库表名,不设置时Django默认使用 "应用名_模型名"(小写)。ordering 设置默认排序字段,支持前缀 "-" 表示降序。verbose_name 和 verbose_name_plural 设置管理后台中模型的中文显示名。unique_together 设置联合唯一约束。其他常用属性还包括 indexes(自定义索引)、constraints(约束条件)以及 abstract(抽象模型基类)等。
要点: 模型字段选项中的 null 和 blank 经常被混淆。null 是数据库层面的概念,blank 是表单验证层面的概念。对于字符串类型字段(CharField、TextField),建议不要设置 null=True,因为Django存储空字符串的方式更为一致。BooleanField 如果要支持空值应使用 NullBooleanField。
二、数据库迁移
Django的迁移系统是模型定义的配套工具,它将模型定义的变化同步到数据库中。迁移系统会记录每次模型变更并生成可重复执行的迁移文件,使得团队成员之间以及不同环境之间能够保持数据库结构一致。
2.1 核心迁移命令
makemigrations 命令检测模型文件的变化并生成新的迁移文件,迁移文件存放在每个应用的 migrations/ 目录下。migrate 命令将迁移文件中记录的变更应用到数据库,它同时会记录已应用的迁移到 django_migrations 表中。sqlmigrate 命令显示指定迁移将要执行的SQL语句而不实际执行,这在调试和审查时非常有用。showmigrations 命令列出所有迁移及其应用状态(已应用或未应用)。
# 检测模型变更并生成迁移文件
python manage.py makemigrations
# 查看将要执行的SQL(不执行)
python manage.py sqlmigrate blog 0001
# 应用所有未应用的迁移
python manage.py migrate
# 查看所有迁移的状态
python manage.py showmigrations
# 指定应用迁移到特定版本
python manage.py migrate blog 0001
2.2 迁移文件管理
迁移文件是普通的Python文件,存放在每个应用的 migrations/ 目录中。迁移文件应纳入版本控制,确保所有开发环境和生产环境使用相同的迁移历史。在团队协作中,如果遇到迁移冲突,可以使用 --merge 参数合并多个迁移分支,或手动调整依赖关系。不建议在生产环境中删除或修改已应用的迁移文件,这可能导致迁移历史不一致。如果需要回退,应使用 migrate 命令反向迁移。
2.3 数据迁移(RunPython)
除了结构变更,迁移还可以执行数据操作。RunPython 允许在迁移中执行自定义Python代码,用于数据迁移、数据清洗或初始化默认数据。使用 RunPython 时需要提供 forwards 函数和 backwards 函数(用于回退)。这种方式比手动执行数据脚本更加可靠和可重复。
# 生成空的迁移文件
python manage.py makemigrations blog --empty
# 在生成的迁移文件中编写数据迁移
from django.db import migrations
def set_default_category(apps, schema_editor):
Category = apps.get_model('blog', 'Category')
Category.objects.create(name='默认分类', sort_order=0)
def reverse_func(apps, schema_editor):
Category = apps.get_model('blog', 'Category')
Category.objects.filter(name='默认分类').delete()
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.RunPython(set_default_category, reverse_func),
]
经验之谈: 在迁移中使用 apps.get_model() 而非直接 import 模型,这是因为迁移框架需要确保引用的模型版本与迁移执行时的状态一致,而非代码中模型的最新状态。这是Django迁移系统的重要设计原则。
三、CRUD操作
CRUD(Create、Read、Update、Delete)是数据库操作的基础。Django ORM 提供了丰富且直观的API来执行这些操作,同时保持了代码的简洁性和可读性。
3.1 创建记录
Django提供了两种创建记录的方式。第一种是使用模型的构造函数创建实例,然后调用 save() 方法持久化到数据库。第二种是使用模型管理器的 create() 方法一步完成创建和保存。两种方式在功能上等价,但第二种更为简洁。如果需要批量创建大量记录,使用 bulk_create() 方法可以显著提升性能。
# 方式一:构造 + save
article = Article(title='Django入门', content='...', view_count=0)
article.save()
# 方式二:create一步完成
article = Article.objects.create(
title='Django入门',
content='...',
view_count=0
)
# 批量创建
Article.objects.bulk_create([
Article(title='文章1', content='...'),
Article(title='文章2', content='...'),
])
3.2 查询记录
Django ORM 的查询API设计优雅且功能强大。all() 返回所有记录,get() 返回满足条件的单条记录(不存在或超过一条时抛出异常),filter() 返回满足条件的结果集,exclude() 返回不满足条件的结果集。这些方法返回的都是 QuerySet 对象,支持链式调用。
字段查询是 filter() 和 exclude() 的核心能力,通过在字段名后加双下划线和查询关键字来实现。exact 和 iexact 分别执行精确匹配和大小写不敏感匹配。contains 和 icontains 执行包含匹配。in 检查是否在列表中。gt、gte、lt、lte 执行大小比较。startswith 和 endswith 执行前缀和后缀匹配。range 查询范围内的值。date、year、month、day 对日期字段的各个部分进行过滤。isnull 检查是否为 NULL。
# 常用查询示例
articles = Article.objects.all()
# 字段查询
Article.objects.filter(title__contains='Django')
Article.objects.filter(view_count__gte=100)
Article.objects.filter(publish_date__year=2025)
Article.objects.filter(id__in=[1, 3, 5])
Article.objects.filter(title__startswith='Django')
Article.objects.filter(created_at__range=(start_date, end_date))
Article.objects.filter(category__isnull=True)
# 链式查询(QuerySet可链式调用)
articles = Article.objects.filter(
is_published=True
).exclude(
view_count=0
).order_by('-created_at')[:10]
# 惰性求值:QuerySet只有被迭代时才执行SQL
q = Article.objects.filter(is_published=True)
q = q.filter(view_count__gt=50) # 尚未执行查询
print(q) # 此时才执行SQL
3.3 QuerySet的惰性求值
QuerySet 的一个重要特性是惰性求值(Lazy Evaluation)。创建 QuerySet 时并不会立即执行数据库查询,只有在"需要结果"的时刻才会真正执行SQL。触发求值的操作包括:迭代(for循环)、切片(带步长)、打印(repr/str)、转换为列表(list())、布尔测试(if queryset)、len() 等。利用惰性求值可以做复杂的链式过滤而不会产生额外查询。
3.4 更新和删除
更新记录有两种方式。实例级别的更新:先查询出对象,修改属性后调用 save()。批量更新:使用 QuerySet 的 update() 方法一次性更新多条记录,只执行一条SQL语句。删除记录也类似:实例的 delete() 删除单条记录,QuerySet 的 delete() 批量删除。需要注意,delete() 操作是不可逆的,且会触发级联删除。
# 更新:实例.save()
article = Article.objects.get(id=1)
article.title = '新标题'
article.save()
# 更新:批量update()
Article.objects.filter(
view_count=0
).update(is_published=False)
# 删除
article = Article.objects.get(id=1)
article.delete()
# 批量删除
Article.objects.filter(is_published=False).delete()
四、模型关系
真实业务场景中的数据很少是孤立的,Django ORM 提供了三种关系字段来建模数据之间的关联:ForeignKey(多对一)、ManyToManyField(多对多)和 OneToOneField(一对一)。
4.1 ForeignKey 一对多关系
ForeignKey 用于表示多对一关系,例如多篇文章属于同一个分类。外键字段在数据库层面会创建一个外键约束。on_delete 参数指定当关联对象被删除时当前对象的行为:CASCADE 级联删除、PROTECT 阻止删除(抛出 ProtectedError)、SET_NULL 设置为 NULL(需 null=True)、SET_DEFAULT 设为默认值、DO_NOTHING 不做任何操作。
class Article(models.Model):
title = models.CharField(max_length=200)
category = models.ForeignKey(
'Category',
on_delete=models.SET_NULL,
null=True,
related_name='articles'
)
class Category(models.Model):
name = models.CharField(max_length=100)
# 正向查询:从文章获取分类
article.category.name
# 反向查询:从分类获取所有文章
category.articles.all() # related_name 指定
Category.objects.first().article_set.all() # 默认名称
4.2 ManyToManyField 多对多关系
ManyToManyField 用于表示多对多关系,例如一篇文章可以有多个标签,一个标签也可以对应多篇文章。Django会自动创建一张中间表来维护关系。如果需要为中间表添加额外字段(如关联时间、排序等),可以使用 through 参数指定自定义中间模型。
class Article(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField('Tag', related_name='articles')
class Tag(models.Model):
name = models.CharField(max_length=50)
# 添加关系
article.tags.add(tag1, tag2)
# 移除关系
article.tags.remove(tag1)
# 清除所有关系
article.tags.clear()
# 自定义中间表
class Article(models.Model):
tags = models.ManyToManyField('Tag', through='ArticleTag')
class ArticleTag(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'blog_article_tag'
4.3 OneToOneField 一对一关系
OneToOneField 用于表示一对一关系,本质上是对 ForeignKey 加上了 unique=True 约束。最常见的应用场景是扩展内置 User 模型,为每个用户添加额外的个人信息字段。
from django.contrib.auth.models import User
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/')
phone = models.CharField(max_length=20, blank=True)
# 使用方式
user.profile.bio
# 反向也是直接的(因为是一对一)
Profile.objects.select_related('user').first()
4.4 预加载与性能优化
ORM 中常见的性能问题是 N+1 查询问题。例如,循环显示文章及其分类时,如果每篇文章都执行一次分类查询,会导致大量数据库查询。select_related() 通过 SQL JOIN 一次性加载关联对象,适用于 ForeignKey 和 OneToOneField 关系。prefetch_related() 通过额外查询后由Python完成关联,适用于 ManyToManyField 和反向查询。合理使用预加载可以显著减少数据库查询次数。
# N+1问题:循环中每次都会执行查询
articles = Article.objects.all()
for article in articles:
print(article.category.name) # 每次循环都查询一次
# select_related:JOIN一次查询(适用于ForeignKey)
articles = Article.objects.select_related('category').all()
# prefetch_related:两次查询,Python合并(适用于ManyToMany)
articles = Article.objects.prefetch_related('tags').all()
五、聚合与注解
Django ORM 提供了强大的聚合查询功能,允许对数据集进行统计计算。聚合(aggregate)对整个 QuerySet 进行计算,返回一个字典。注解(annotate)为 QuerySet 中的每个对象添加计算后的字段值。
5.1 聚合函数
Django 提供了五个聚合函数:Sum(求和)、Count(计数)、Avg(平均值)、Min(最小值)和 Max(最大值)。这些函数位于 django.db.models 模块中。aggregate() 方法接收一个或多个聚合函数,返回包含计算结果的字典。
from django.db.models import Sum, Count, Avg, Min, Max
# 所有文章的浏览量总和
result = Article.objects.aggregate(
total_views=Sum('view_count')
)
# 结果:{'total_views': 12345}
# 多个聚合
stats = Article.objects.aggregate(
total=Sum('view_count'),
average=Avg('view_count'),
max_views=Max('view_count'),
min_views=Min('view_count'),
count=Count('id')
)
5.2 分组注解
annotate() 为 QuerySet 中的每一个对象添加聚合计算的结果,常用于分组统计。配合 values() 使用时,annotate() 会按照 values() 指定的字段进行分组。这在生成报表和数据统计时非常实用。
from django.db.models import Count
# 每篇文章的标签数量
articles = Article.objects.annotate(
tag_count=Count('tags')
)
for article in articles:
print(article.title, article.tag_count)
# 按分类分组统计文章数
category_stats = Category.objects.annotate(
article_count=Count('articles'),
total_views=Sum('articles__view_count')
)
# 使用values分组
from django.db.models import Count, Avg
stats = Article.objects.values(
'category__name'
).annotate(
count=Count('id'),
avg_views=Avg('view_count')
).order_by('-count')
5.3 values() 与 values_list()
values() 返回 QuerySet 中字典的列表,只包含指定的字段。values_list() 返回元组列表,当 flat=True 时返回单个值的列表。这两个方法可以减少查询返回的数据量,提升性能。与 annotate() 结合使用时,values() 起到分组的作用。
# 只获取部分字段
Article.objects.values('id', 'title')
# [{'id': 1, 'title': 'Django入门'}, ...]
Article.objects.values_list('id', 'title')
# [(1, 'Django入门'), ...]
Article.objects.values_list('title', flat=True)
# ['Django入门', '进阶', ...]
5.4 排序与去重
order_by() 对查询结果进行排序,支持按多个字段排序,字段名加前缀 "-" 表示降序。order_by('?') 实现随机排序(注意:性能开销较大)。distinct() 去除查询结果中的重复行,在跨表查询时尤其有用。
# 排序
Article.objects.order_by('-view_count', 'title')
# 随机排序(谨慎使用,大数据集性能差)
Article.objects.order_by('?')
# 去重
Article.objects.filter(
tags__name__in=['Python', 'Django']
).distinct()
六、Q对象与F对象
Q对象和F对象是Django ORM中两个非常实用的工具,分别用于解决复杂查询条件和字段间引用的场景。掌握它们可以大幅提升查询的灵活性和效率。
6.1 Q对象:复杂逻辑查询
Q对象封装了SQL查询中的条件表达式,支持使用位运算符组合多个条件。默认情况下 filter() 的多个参数之间是 AND 关系,Q对象则允许构建任意复杂的查询逻辑,包括 OR、AND 和 NOT。|(管道符)表示 OR 逻辑,&(与号)表示 AND 逻辑,~(波浪号)表示 NOT 逻辑。Q对象可以与普通关键字参数混合使用,但 Q 对象必须放在关键字参数之前。
from django.db.models import Q
# OR查询:浏览量大于1000 或者 标题包含"Django"
Article.objects.filter(
Q(view_count__gt=1000) | Q(title__contains='Django')
)
# AND与NOT组合
Article.objects.filter(
Q(is_published=True) & ~Q(category__isnull=True)
)
# 复杂条件组合
Article.objects.filter(
Q(category__name='Python') | Q(category__name='Django'),
is_published=True,
view_count__gte=100
)
# 动态构建查询条件
query = Q()
if search_keyword:
query &= Q(title__icontains=search_keyword)
if category_id:
query &= Q(category_id=category_id)
if min_views:
query &= Q(view_count__gte=min_views)
articles = Article.objects.filter(query)
6.2 F对象:字段引用
F对象用于在数据库层面引用模型字段的值,而不是在Python内存中操作。这在更新操作中至关重要:使用 F 对象可以避免竞态条件(Race Condition)。比如在多并发场景下增加浏览量,如果先用Python读取再写回,会存在数据不一致的风险,而 F 对象将操作下推到数据库层执行,保证原子性。F 对象也支持算术运算,可以用在查询条件中比较两个字段。
from django.db.models import F
# 浏览量加1(原子操作,无竞态条件)
Article.objects.filter(id=1).update(view_count=F('view_count') + 1)
# 在查询条件中比较两个字段
Article.objects.filter(
view_count__gt=F('comment_count') * 10
)
# F对象与算术运算
Article.objects.update(
popularity=F('view_count') + F('comment_count') * 2
)
# 字符串操作(Django 1.11+)
from django.db.models.functions import Concat
Article.objects.update(
title=Concat(F('title'), models.Value(' [已更新]'))
)
6.3 Q与F结合使用
Q对象和F对象可以无缝结合,实现非常灵活的查询和更新逻辑。例如,查找那些浏览量远高于评论数的热门文章,并一次性增加其热度权重,这些都可以在一个查询中完成。
# 查找热门文章并更新
Article.objects.filter(
Q(view_count__gt=F('comment_count') * 5) |
Q(view_count__gt=10000)
).update(is_recommended=True)
性能提示: 使用 F 对象进行更新操作比先查询再 Python 层面修改要高效得多,因为它只执行一条 SQL UPDATE 语句,避免了 SELECT + UPDATE 两条语句的开销,更重要的是它避免了并发场景下的竞态条件。
七、事务管理
事务是数据库操作的基本单元,它将一组数据库操作组合为一个不可分割的工作单元。事务的 ACID 特性(原子性、一致性、隔离性、持久性)确保了数据的完整性和可靠性。Django 提供了简洁的事务管理机制。
7.1 装饰器方式:@transaction.atomic
使用 @transaction.atomic 装饰器可以将视图函数或任意函数包裹在事务中。如果函数体中的任一数据库操作失败,整个事务中的所有操作都会被回滚,数据库恢复到事务开始前的状态。这在涉及多个关联操作(如创建订单的同时扣减库存)时至关重要。
from django.db import transaction
@transaction.atomic
def create_order(user, product_id, quantity):
product = Product.objects.select_for_update().get(id=product_id)
if product.stock < quantity:
raise ValueError('库存不足')
Order.objects.create(
user=user,
product=product,
quantity=quantity,
total=product.price * quantity
)
product.stock -= quantity
product.save()
# 如果上面任何一步失败,所有操作自动回滚
7.2 上下文管理器方式
使用 with transaction.atomic(): 上下文管理器可以更精确地控制事务范围。只有 with 块内的代码在事务保护下,块外的代码不受影响。上下文管理器也支持嵌套,内层的事务会合并到外层事务中。
def batch_publish(category_id):
with transaction.atomic():
articles = Article.objects.filter(
category_id=category_id,
is_published=False
)
for article in articles:
article.is_published = True
article.publish_date = timezone.now()
article.save()
# 所有保存操作作为一个原子事务
# 此处代码已不在事务中
7.3 保存点(Savepoint)
保存点允许在事务内部创建回滚点,实现部分回滚。这在需要在一个大事务中处理多个独立操作时非常有用。Django 通过 transaction.savepoint() 及其相关方法提供了保存点的完整支持。如果某组操作失败,可以只回滚到最近的保存点,而不是回滚整个事务。
from django.db import transaction
def process_batch(records):
with transaction.atomic():
# 创建保存点
sid = transaction.savepoint()
try:
for record in records:
Article.objects.create(**record)
except Exception:
# 回滚到此保存点,不影响事务中已提交的其他操作
transaction.savepoint_rollback(sid)
log_error('部分记录创建失败')
raise
# 确认保存点,释放资源
transaction.savepoint_commit(sid)
7.4 事务隔离级别与锁
在高并发场景下,可能需要控制事务的隔离级别或使用行级锁。Django 的 select_for_update() 方法在事务中执行行级锁(SELECT ... FOR UPDATE),阻止其他事务同时修改相同的行,直到当前事务提交。这在实现库存扣减、账户转账等需要数据一致性的场景中非常关键。值得注意的是,select_for_update() 必须在事务中使用,且仅对支持行级锁的数据库(如 PostgreSQL、MySQL)有效。
总结: 事务管理的核心原则是"要么全部成功,要么全部失败"。在 Web 开发中,任何涉及多个数据库写操作且要求数据一致性的场景都应该使用事务。结合 select_for_update() 行级锁,可以有效防止高并发下的数据竞争问题。但需要注意,长时间持有数据库锁会影响系统吞吐量,事务应尽量保持简短。