Django 模板复用、ORM 查询与多对多关系
本章包括两类高频能力:一是通过 inclusion tag 和模板继承复用页面片段与整体布局;二是通过 ORM 查询、字段查找和关系模型组织数据。它们分别解决"页面不要重复写"和"数据不要重复存"的问题。
一、自定义 inclusion tag:复用局部模板
当多个页面共享一小块结构,但显示的数据不同,例如公告栏、推荐商品、分页器或广告位,可以使用 inclusion tag。它由 Python 函数准备上下文,再渲染一个指定的局部模板;比复制 HTML 更易维护。
创建模板标签库
模板标签模块必须位于已安装 App 的 templatetags/ 目录中,且目录需要包含 __init__.py,以便 Django 将它识别为 Python 包。
text
shop/
├── templatetags/
│ ├── __init__.py
│ └── shop_tags.py
└── templates/
└── shop/
└── includes/
└── product_cards.html
python
# shop/templatetags/shop_tags.py
from django import template
register = template.Library()
@register.inclusion_tag("shop/includes/product_cards.html")
def product_cards(products, title="推荐商品"):
"""返回局部模板渲染所需的上下文。"""
return {
"products": products,
"title": title,
}
html
<!-- shop/templates/shop/includes/product_cards.html -->
<section class="product-cards">
<h2>{{ title }}</h2>
<ul>
{% for product in products %}
<li>{{ product.name }}:{{ product.price }}</li>
{% empty %}
<li>暂无商品</li>
{% endfor %}
</ul>
</section>
代码说明:@register.inclusion_tag() 参数是局部模板的名称。标签函数返回字典,该字典就是局部模板上下文;不要返回 locals(),以免把无关变量暴露给模板。
在页面中使用 inclusion tag
html
{% load shop_tags %}
<main>
<h1>首页</h1>
{% product_cards featured_products "本周精选" %}
</main>
代码说明:{% load shop_tags %} 加载的是 Python 模块名,不含 .py 后缀。修改 templatetags 目录结构或新增标签模块后,开发服务器有时需要重启才能重新发现标签库。
二、模板继承:复用整体页面布局
inclusion tag 用于复用局部片段;模板继承用于复用页面骨架,例如 HTML 文档结构、导航、页脚和公共静态资源。父模板用 {% block %} 留出可替换区域,子模板用 {% extends %} 继承父模板并重写对应块。
html
<!-- templates/base.html -->
{% load static %}
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{% block title %}站点首页{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/site.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>公共导航</header>
<main>
{% block content %}
<p>默认页面内容</p>
{% endblock %}
</main>
<footer>公共页脚</footer>
{% block extra_js %}{% endblock %}
</body>
</html>
html
<!-- shop/templates/shop/product_list.html -->
{% extends "base.html" %}
{% block title %}商品列表{% endblock %}
{% block content %}
<h1>商品列表</h1>
{% for product in products %}
<article>{{ product.name }}</article>
{% empty %}
<p>暂无商品。</p>
{% endfor %}
{% endblock %}
代码说明:{% extends %} 通常应放在子模板第一行。块名需要在父子模板中一致;子模板只替换声明的块,未重写部分继续使用父模板内容。
保留父模板块内容
当子模板需要在父模板已有内容基础上追加,而非完全替换时,可使用 {``{ block.super }}。
html
{% block extra_js %}
{{ block.super }}
<script src="/static/js/product-list.js"></script>
{% endblock %}
代码说明:公共资源最好仍使用 {% static %} 生成路径。block.super 只在继承块内部可用,表示父模板中同名块的原始内容。
三、模型设计与时间字段
模型应位于各 App 的 models.py,并通过迁移同步到数据库。字段类型要符合数据含义:金额使用 DecimalField,而不是 CharField 或浮点数;创建时间和更新时间的自动行为也不同。
python
# library/models.py
from django.db import models
class User(models.Model):
name = models.CharField(max_length=32, verbose_name="姓名")
age = models.PositiveIntegerField(verbose_name="年龄")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="注册时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = "user"
ordering = ["-created_at"]
def __str__(self):
return self.name
代码说明:auto_now_add=True 仅在对象第一次创建时自动写入时间;auto_now=True 会在每次调用实例 save() 时更新。批量 QuerySet.update() 不会触发 auto_now,因为它不会调用每个实例的 save() 方法。
数据库配置原则
数据库密码不应直接写入或提交到代码仓库。以下是 MySQL 配置结构示例,实际密码应通过环境变量或密钥管理服务读取。
python
# settings.py
import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "library_db",
"USER": os.environ.get("DB_USER", "library_user"),
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": "127.0.0.1",
"PORT": "3306",
"OPTIONS": {"charset": "utf8mb4"},
}
}
四、QuerySet 常用操作与返回类型
objects 管理器生成的查询对象叫 QuerySet。它具有惰性:构造查询时通常不会立即访问数据库,遍历、转换为 list()、调用 first()、count() 等操作时才会执行 SQL。
python
from library.models import User
# 创建记录
user = User.objects.create(name="dream", age=18)
# 查询:都返回 QuerySet
all_users = User.objects.all()
adults = User.objects.filter(age__gte=18)
non_dream_users = User.objects.exclude(name="dream")
# 单条结果或 None
first_adult = adults.first()
last_adult = adults.last()
# get() 期望恰好一条记录,否则抛出异常
one_user = User.objects.get(pk=user.pk)
代码说明:all()、filter()、exclude() 返回 QuerySet,不是 Python 列表;可以继续链式追加筛选和排序。get() 找不到时抛出 DoesNotExist,多条匹配时抛出 MultipleObjectsReturned,因此仅用于结果唯一的场景。
更新、删除与统计
python
# 批量更新,返回受影响行数
User.objects.filter(name="dream").update(age=19)
# 实例更新,可触发 save() 相关逻辑
user = User.objects.get(pk=1)
user.age = 20
user.save(update_fields=["age"])
# 删除所有匹配记录
User.objects.filter(name="obsolete").delete()
# 统计和存在性判断
total = User.objects.count()
has_dream = User.objects.filter(name="dream").exists()
代码说明:只需要判断是否存在时使用 exists(),比查询全部记录再判断效率更高。update() 和 delete() 都可能影响多条数据,执行前应确认筛选条件。生产系统的敏感删除通常还需要权限、审计和软删除策略。
values()、values_list() 与排序
当只需要部分字段而不需要模型实例时,使用 values() 或 values_list() 能减少对象构造开销。distinct() 去重的行为受数据库和 order_by() 影响,应在目标数据库中验证最终 SQL。
python
# 字典形式的 QuerySet
names_and_ages = User.objects.values("name", "age")
# 元组形式的 QuerySet;flat=True 只适用于单字段
names = User.objects.values_list("name", flat=True)
# 排序:升序、降序和反转已排序结果
young_first = User.objects.order_by("age")
old_first = User.objects.order_by("-age")
reversed_users = User.objects.order_by("age").reverse()
# 对选定字段去重
unique_names = User.objects.values("name").distinct()
print(User.objects.filter(age__gte=18).query)
代码说明:values() 的每项是字典,values_list() 的每项默认是元组。.query 适合开发调试时查看 ORM 生成的 SQL 表达式,但不要依赖其字符串格式实现业务逻辑。
五、双下划线字段查找
Django 使用 字段名__查找类型 表达查询条件。双下划线既可以表示比较方式,也可跨越关联关系。以下示例以 User.age 和 User.name 为例。
python
from datetime import date
from library.models import User
# 数值比较
User.objects.filter(age__gt=18)
User.objects.filter(age__gte=18)
User.objects.filter(age__lt=60)
User.objects.filter(age__lte=60)
# 多值与范围;range 包含两个边界
User.objects.filter(age__in=[18, 20, 22])
User.objects.filter(age__range=(18, 30))
# 文本匹配。大小写敏感性也会受具体数据库排序规则影响。
User.objects.filter(name__contains="dream")
User.objects.filter(name__icontains="DREAM")
User.objects.filter(name__startswith="d")
User.objects.filter(name__endswith="m")
# 日期字段的组成部分与范围查询
User.objects.filter(created_at__year=2026)
User.objects.filter(created_at__date=date(2026, 7, 24))
User.objects.filter(created_at__date__range=(date(2026, 7, 1), date(2026, 7, 31)))
代码说明:range 是包含边界的。日期时间字段按 __day、__month 等单独查询可能让数据库难以利用索引;在数据量较大时,优先使用明确的日期或时间范围查询。查询含有用户输入的文本时,ORM 会参数化处理,不要拼接原始 SQL。
六、关系模型:一对一、一对多与多对多
关系字段避免重复存储关联数据。Django 中常用 OneToOneField、ForeignKey 和 ManyToManyField 表达三类关系:
| 关系 | Django 字段 | 示例 |
|---|---|---|
| 一对一 | OneToOneField |
用户与扩展资料 |
| 一对多 | ForeignKey |
部门与员工、出版社与图书 |
| 多对多 | ManyToManyField |
图书与作者、学生与课程 |
多对多方案一:只显式定义中间模型
当关系本身需要额外数据(例如作者在一本书中的署名、排序、加入时间)时,应显式定义中间模型。这个方案最灵活,但不会自动得到 book.authors.add() 这类多对多管理器。
python
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
class Author(models.Model):
name = models.CharField(max_length=32)
class BookAuthor(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
pen_name = models.CharField(max_length=64, blank=True)
position = models.PositiveSmallIntegerField(default=1)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["book", "author"],
name="unique_book_author",
),
]
代码说明:中间模型是正常的数据表,能保存关系的额外属性。UniqueConstraint 防止同一作者被重复关联到同一本书。on_delete=models.CASCADE 表示删除图书或作者时,相关中间记录也会删除;这不是"级联更新"。
多对多方案二:显式中间模型 + through
若既需要中间表字段,又希望从 Book 或 Author 方便地访问关联对象,可在 ManyToManyField 中通过 through 指定中间模型。
python
class Book(models.Model):
title = models.CharField(max_length=255)
authors = models.ManyToManyField(
"Author",
through="BookAuthor",
related_name="books",
)
class Author(models.Model):
name = models.CharField(max_length=32)
class BookAuthor(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
role = models.CharField(max_length=32, default="author")
python
book = Book.objects.get(pk=1)
author = Author.objects.get(pk=1)
# through 模型含有额外字段时,直接创建中间记录最明确
BookAuthor.objects.create(book=book, author=author, role="translator")
print(book.authors.all())
print(author.books.all())
代码说明:指定 through 后,不应假设 book.authors.add(author) 一定可用;当中间模型有额外必填字段时,应显式创建 BookAuthor,或在满足条件时传入 through_defaults。若中间模型含有多个指向同一模型的外键,需要再使用 through_fields 消除歧义。
多对多方案三:让 Django 自动创建中间表
若关系本身没有额外字段,最简单的方式是直接声明 ManyToManyField,Django 会自动创建中间表和管理器方法。
python
class Course(models.Model):
name = models.CharField(max_length=100)
class Student(models.Model):
name = models.CharField(max_length=32)
courses = models.ManyToManyField(Course, related_name="students")
python
student = Student.objects.get(pk=1)
course = Course.objects.get(pk=1)
student.courses.add(course)
student.courses.remove(course)
student.courses.set([course])
student.courses.clear()
代码说明:自动中间表适合纯关联关系,使用最简单。关系管理器的 add()、remove()、set() 和 clear() 会直接维护中间表;关联的两端对象必须已经保存到数据库。
七、实践要点
- 局部页面片段使用 inclusion tag,整体布局使用模板继承。
- 模型的金额字段使用
DecimalField,时间字段要区分auto_now_add和auto_now。 values()返回字典形式的QuerySet,values_list()返回元组形式;两者仍是惰性查询对象。- 双下划线查询是 ORM 的统一表达方式,数据量大时优先考虑索引和范围查询。
- 没有额外属性的多对多关系让 Django 自动建表;关系需要保存额外信息时,建立显式中间模型并使用
through。
完成模型修改后,始终运行 python manage.py makemigrations 与 python manage.py migrate,并将生成的迁移文件提交到版本控制。