深入解析Django ORM:模型构建、跨表查询与高级操作
一、 数据库模型设计与关系映射
在Django中,关系型数据库的表结构通过Python类来定义。表与表之间的关联主要分为一对一(One-to-One)、一对多(One-to-Many)以及多对多(Many-to-Many)。Django的ORM(对象关系映射)提供了丰富的字段类型和参数来处理这些复杂的业务逻辑。
1. 模型定义与外键配置
以下示例构建了文章(Article)、作者(Writer)、出版社(Publisher)及作者详情(WriterProfile)的完整关系网:
from django.db import models
class Publisher(models.Model):
pub_id = models.AutoField(primary_key=True)
company_name = models.CharField(max_length=50, verbose_name="出版社名称")
location = models.CharField(max_length=50, verbose_name="所在城市")
contact_email = models.EmailField(verbose_name="联系邮箱")
class WriterProfile(models.Model):
profile_id = models.AutoField(primary_key=True)
birth_date = models.DateField(verbose_name="出生日期")
biography = models.TextField(verbose_name="个人简介")
class Writer(models.Model):
writer_id = models.AutoField(primary_key=True)
full_name = models.CharField(max_length=50, verbose_name="作者姓名")
# 一对一关系:外键可建立在任意一方,底层通过UNIQUE约束实现
profile = models.OneToOneField(
WriterProfile,
on_delete=models.CASCADE,
related_name='writer_info'
)
class Article(models.Model):
article_id = models.AutoField(primary_key=True)
headline = models.CharField(max_length=100, verbose_name="文章标题")
release_date = models.DateField(verbose_name="发布日期")
cost = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="定价")
views = models.IntegerField(default=0, verbose_name="阅读量")
likes = models.IntegerField(default=0, verbose_name="点赞数")
# 一对多关系:建立在"多"的一方,db_constraint=False可解除数据库层面的物理外键约束,但保留ORM层面的操作能力
publisher = models.ForeignKey(
Publisher,
on_delete=models.SET_NULL,
null=True,
db_constraint=False,
related_name='articles'
)
# 多对多关系:指定自定义的中间表
writers = models.ManyToManyField(
Writer,
through='ArticleWriterRel',
through_fields=('article', 'writer'),
related_name='published_articles'
)
2. 多对多关系的建表策略
处理多对多关联时,有三种常见的实现方式:
- 全自动模式:直接使用
ManyToManyField,Django会在迁移时自动生成第三张关联表。缺点是关联表结构固定,无法扩展额外字段。 - 纯手动模式:完全通过两个
ForeignKey建立第三张表。这种方式灵活性高,但无法使用ORM提供的add()、set()等便捷API。 - 中介模型模式(推荐):如上例所示,手动创建包含两个外键的模型(如
ArticleWriterRel),并通过through参数将其与ManyToManyField绑定。这既保留了多对多API,又允许在中间表中存储额外数据(如作者贡献角色)。
class ArticleWriterRel(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE)
writer = models.ForeignKey(Writer, on_delete=models.CASCADE)
role_type = models.CharField(max_length=20, default='Main Author')
class Meta:
db_table = 'custom_mapping_table'
unique_together = ('article', 'writer') # 联合唯一索引
3. 外键级联行为与元信息
on_delete参数决定了当关联的主表数据被删除时,从表数据的命运:
models.CASCADE:级联删除关联数据。models.PROTECT:阻止删除,抛出ProtectedError。models.SET_NULL:将外键字段置为NULL(需配合null=True)。models.SET_DEFAULT:恢复为默认值。
通过内部的Meta类,可以精细控制模型的数据库行为,例如使用db_table覆写默认表名,或使用ordering指定默认排序规则。
二、 关联数据的增删改操作
1. 一对多数据的创建
可以通过传递对象实例或直接传递主键ID来建立关联:
# 方式一:传递Publisher对象实例
pub_obj = Publisher.objects.get(pub_id=1)
Article.objects.create(
headline="深入理解并发编程",
release_date="2023-11-01",
cost=89.00,
publisher=pub_obj
)
# 方式二:直接赋值外键ID(Django自动在内存中附加_id后缀)
Article.objects.create(
headline="微服务架构实战",
release_date="2023-11-05",
cost=120.00,
publisher_id=1
)
2. 多对多关系的管理API
对于多对多关系,必须通过ORM提供的专属方法操作隐式的中间表:
article = Article.objects.get(article_id=1)
writer_alice = Writer.objects.get(full_name="Alice")
writer_bob = Writer.objects.get(full_name="Bob")
# 添加关联
article.writers.add(writer_alice, writer_bob)
article.writers.add(3, 4) # 直接传入主键ID
# 批量覆盖关联(先清空现有关系,再重新绑定)
article.writers.set([writer_alice])
# 移除特定关联
article.writers.remove(writer_bob)
# 清空所有关联
article.writers.clear()
三、 跨表查询机制
Django ORM提供了两种跨表查询的核心思路:基于对象的查询(执行多次SQL)和基于双下划线的查询(通过SQL JOIN实现)。
1. 基于对象的查询
通过模型属性访问关联数据。正向查询使用外键字段名,反向查询使用小写模型名加_set(若配置了related_name则使用自定义名称)。
# 正向查询:获取文章对应的出版社信息
article = Article.objects.get(headline="微服务架构实战")
print(article.publisher.company_name)
# 反向查询:获取某出版社发行的所有文章
tech_press = Publisher.objects.get(company_name="TechPress")
article_list = tech_press.articles.all() # 使用了 related_name='articles'
for art in article_list:
print(art.headline)
2. 基于双下划线的JOIN查询
双下划线__不仅能连接多张表,还能作为字段查找的后缀。正向跨表使用字段名,反向跨表使用表名小写。
# 查询特定出版社发行的所有文章标题与定价
Article.objects.filter(
publisher__company_name="TechPress"
).values_list('headline', 'cost')
# 反向JOIN:查询发行了包含"Django"关键字文章的出版社名称
Publisher.objects.filter(
articles__headline__icontains="Django"
).values_list('company_name', flat=True)
# 连续跨表:查询1990年出生的作者所写文章的出版社位置
Article.objects.filter(
writers__profile__birth_date__year=1990
).values('headline', 'publisher__location')
四、 聚合查询与复杂过滤
1. 聚合(Aggregate)与分组(Annotate)
aggregate()是QuerySet的终止子句,返回包含统计结果的字典;而annotate()则为查询集中的每个对象附加一个计算属性。
from django.db.models import Avg, Max, Count, Sum
# 聚合:计算所有文章的平均定价
result = Article.objects.aggregate(avg_cost=Avg('cost'))
# 返回: {'avg_cost': 65.50}
# 分组统计:统计每个出版社的文章数量与最高定价
# 注意:values()在annotate()之前,表示按该字段进行GROUP BY
Publisher.objects.values('company_name').annotate(
total_articles=Count('articles'),
max_price=Max('articles__cost')
)
# 筛选分组后的结果:找出拥有超过5位合作作者的文章
Article.objects.annotate(
author_count=Count('writers')
).filter(author_count__gt=5).values('headline', 'author_count')
2. F表达式
F对象允许在数据库层面直接比较同一模型内不同字段的值,或执行批量数学运算,避免将数据加载到Python内存中。
from django.db.models import F
# 筛选阅读量低于点赞数10倍的文章
Article.objects.filter(views__lt=F('likes') * 10)
# 批量更新:所有文章定价上调15元
Article.objects.update(cost=F('cost') + 15)
3. Q对象与复杂逻辑组合
默认的filter()参数之间是AND关系。使用Q对象可以构建包含OR(|)、NOT(~)的复杂WHERE子句。
from django.db.models import Q
# 查询2023年发布,或者定价低于20元,且出版社位于北京的文章
queryset = Article.objects.filter(
Q(release_date__year=2023) | Q(cost__lt=20),
publisher__location='Beijing' # Q对象必须放在关键字参数之前
)
五、 原生SQL执行与系统级集成
1. 执行原生SQL
当ORM无法表达极端复杂的查询时,可退化为原生SQL。使用raw()方法可以将结果映射回模型实例,前提是SELECT语句必须包含主键。
# 使用raw执行查询
articles = Article.objects.raw('SELECT article_id, headline, cost FROM myapp_article WHERE cost > %s', [50])
for art in articles:
print(art.headline)
# 绕过模型层,直接使用数据库游标执行UPDATE/DELETE
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("UPDATE myapp_article SET cost = cost * 0.9 WHERE release_date < %s", ['2020-01-01'])
2. 在外部脚本中调用Django环境
若需在定时任务或独立的Python脚本中使用Django模型,必须先初始化Django的配置环境。
import os
import django
# 指向项目的核心配置文件
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core_project.settings")
django.setup()
# 环境初始化后方可导入models
from blog.models import Article
active_count = Article.objects.filter(is_published=True).count()
print(f"Active articles: {active_count}")
3. 跨应用(App)模型关联
在大型项目中,模型通常分散在多个App内。若要在一个App中关联另一个App的模型,推荐使用字符串形式引用,以避免循环导入问题。
# 在 app_b 的 models.py 中
from django.db import models
class Category(models.Model):
category_name = models.CharField(max_length=30)
# 使用 'app_label.ModelName' 格式进行跨应用关联
featured_writer = models.ForeignKey(
'app_a.Writer',
on_delete=models.SET_NULL,
null=True
)
执行数据库迁移命令后,Django会自动解析字符串并建立正确的跨表物理外键约束。