下载工作台
Django Web 开发

ORM 进阶与查询优化

试读上半部分 · 解锁后可读全文

第 5 章 · ORM 进阶与查询优化

本章目标:深入 Django ORM 的关系型建模ForeignKeyManyToManyField);掌握 select_relatedprefetch_related 解决 N+1 查询;使用 transaction.atomic 保证数据一致性;了解数据库索引Meta.indexes;能在 shop-demo 中建模「商品–分类–标签–订单行」并完成高效查询。

学时建议:5~6 小时(含 2 小时跟练与 SQL 观察)

前置:完成 django-web ch01~ch04ProductCategory 模型、CBV 列表)。


5.1 场景说明:商品关联越来越复杂

shop-demo 电商后台需要表达更多业务关系:

关系类型示例
商品 → 分类多对一(N:1)每个商品属于一个分类
商品 ↔ 标签多对多(M:N)「热销」「新品」可同时打在多个商品上
订单 → 订单行 → 商品一对多链式下单时扣减库存需事务
列表页展示分类名 + 标签联表查询避免 N+1 性能问题
本章在本地 SQLite 上演练;生产环境常用 PostgreSQL,ORM API 一致。联调 API 域名 api.example.com 仅为占位。

5.2 关系字段详解

5.2.1 ForeignKey(多对一)

已在 ch01 使用:

class Product(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,   # 有商品时禁止删分类
        related_name="products",    # 反向:category.products.all()
    )
on_delete行为适用
CASCADE父删子删强依赖、子无独立意义
PROTECT有子记录则禁止删父分类下有商品
SET_NULL父删子字段置 NULL可选关联,需 null=True
SET_DEFAULT父删子字段置默认值有默认外键时

反向查询:

cat = Category.objects.get(slug="digital")
cat.products.filter(is_active=True)

product = Product.objects.first()
product.category.name

5.2.2 ManyToManyField(多对多)

catalog/models.py 扩展:

class Tag(models.Model):
    name = models.CharField("标签名", max_length=32, unique=True)
    slug = models.SlugField(max_length=32, unique=True)

    class Meta:
        verbose_name = "商品标签"

    def __str__(self):
        return self.name


class Product(models.Model):
    # ... 原有字段 ...
    tags = models.ManyToManyField(Tag, blank=True, related_name="products")

Django 自动创建中间表 catalog_product_tags

catalog_product          catalog_product_tags          catalog_tag
┌────┬─────────┐         ┌────────────┬────────┐      ┌────┬──────┐
│ id │ name    │         │ product_id │ tag_id │      │ id │ name │
├────┼─────────┤         ├────────────┼────────┤      ├────┼──────┤
│ 1  │ 鼠标    │◄───────►│ 1          │ 2      │◄────►│ 2  │ 热销 │
└────┴─────────┘         └────────────┴────────┘      └────┴──────┘

操作示例:

hot = Tag.objects.create(name="热销", slug="hot")
product.tags.add(hot)
product.tags.remove(hot)
product.tags.set([tag1, tag2])
Tag.objects.filter(products__sku="SKU-001")

5.2.3 显式 through 模型(预习)

中间表需额外字段(如打标时间)时,用 through="ProductTag" 自定义中间模型(详见官方文档)。


5.3 订单模型与一对多链

orders/models.py(新建 App orders):

from django.db import models
from catalog.models import Product


class Order(models.Model):
    order_no = models.CharField("订单号", max_length=32, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    status = models.CharField(
        max_length=16,
        choices=[("pending", "待支付"), ("paid", "已支付"), ("cancelled", "已取消")],
        default="pending",
    )

    def __str__(self):
        return self.order_no


class OrderLine(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="lines")
    product = models.ForeignKey(Product, on_delete=models.PROTECT)
    quantity = models.PositiveIntegerField(default=1)
    unit_price = models.DecimalField(max_digits=10, decimal_places=2)

    class Meta:
        verbose_name = "订单明细"

注册 App 并迁移:

python manage.py startapp orders
# settings.INSTALLED_APPS 加入 "orders"
python manage.py makemigrations
python manage.py migrate

链式查询:

Order.objects.filter(lines__product__category__slug="digital").distinct()

5.4 N+1 问题与 select_related

5.4.1 问题演示

# 坏:每条商品额外查一次 category
for p in Product.objects.all():
    print(p.category.name)  # 每次访问触发 SQL

若 100 个商品 → 1 + 100 = 101 次查询(N+1)。

5.4.2 select_related:SQL JOIN

适用于 ForeignKeyOneToOne(单值关系):

products = Product.objects.select_related("category").filter(is_active=True)

生成近似 SQL:

SELECT catalog_product.*, catalog_category.*
FROM catalog_product
INNER JOIN catalog_category ON (catalog_product.category_id = catalog_category.id)
WHERE catalog_product.is_active = 1;

ProductListView 中:

def get_queryset(self):
    return (
        Product.objects.filter(is_active=True)
        .select_related("category")
        .order_by("-created_at")
    )
方法关系类型SQL 策略
select_relatedFK、O2OJOIN
prefetch_relatedM2M、反向外键分两次查询 + Python 合并

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。