Django 模板复用、ORM 查询与多对多关系

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.ageUser.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 中常用 OneToOneFieldForeignKeyManyToManyField 表达三类关系:

关系 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

若既需要中间表字段,又希望从 BookAuthor 方便地访问关联对象,可在 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() 会直接维护中间表;关联的两端对象必须已经保存到数据库。

七、实践要点

  1. 局部页面片段使用 inclusion tag,整体布局使用模板继承。
  2. 模型的金额字段使用 DecimalField,时间字段要区分 auto_now_addauto_now
  3. values() 返回字典形式的 QuerySetvalues_list() 返回元组形式;两者仍是惰性查询对象。
  4. 双下划线查询是 ORM 的统一表达方式,数据量大时优先考虑索引和范围查询。
  5. 没有额外属性的多对多关系让 Django 自动建表;关系需要保存额外信息时,建立显式中间模型并使用 through

完成模型修改后,始终运行 python manage.py makemigrationspython manage.py migrate,并将生成的迁移文件提交到版本控制。

相关推荐
TheBestRucy1 小时前
基于Dify的旅游攻略&王者荣耀攻略智能助手项目
服务器·开发语言·人工智能·python·算法·旅游
天才少女爱迪生1 小时前
KIMI-K3技术博客写作思路分析
python
丨白色风车丨1 小时前
MCP 入门指南:大模型时代的“USB-C”接口
python·mcp
TELL5211 小时前
帮我生成对应的sql 以及数据库-deepseek提示词
数据库·sql
能年玲奈喝榴莲牛奶2 小时前
系统规划与管理师-第一章-考点记忆
大数据·数据库·软考·系统规划与管理师·系规
SatanII2 小时前
mariadb
数据库·mariadb
EXI-小洲2 小时前
Web Spider 某渣渣企业平台 表单参数逆向 Webpack
python·webpack·js逆向·spider
玫幽倩2 小时前
2026聚合獬豸杯决赛wp(手机取证)
python·电子取证·misc·取证·聚合·手机取证·獬豸杯
FlyWIHTSKY2 小时前
在智能体系统中,什么是多模态,举例详细说明
人工智能·python·langchain