第 12 章 · Django ORM 对照速通
本章目标:在不强制安装 Django 的前提下,通过 独立可读示例 理解 Django ORM 的 Model、QuerySet、migrate、F()/Q();与 SQLAlchemy 2.0(ch09~ch10)逐项对照;明确 shop_db 字段 slug + is_published + price(分) 在 Django 中的等价写法;衔接 django-web ch05/ch07 与 db-demo。
学时建议:4~5 小时(含 2 小时对照表与练习题)
前置:完成 python-database ch07~ch11;可选阅读 django-web ch05/ch07(非必须)。
12.1 场景说明:同一 shop_db,三种访问方式
shop_db (SQLite / MySQL)
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
sqlite3 ch08 SQLAlchemy ch10 Django ORM
(DB-API) (Session/ORM) (QuerySet)
| 路线 | 项目 | 典型用途 |
|---|---|---|
| 数据库专精 | db-demo | 本章对照 |
| Web 全栈 | shop-demo(django-web) | 后台 + DRF |
| API 服务 | svc-demo(fastapi-web) | SQLAlchemy async |
独立示例:本章代码块标注# django-style处,可复制到shop-demo验证;未装 Django 的学员以对照表 + 伪代码理解即可,不影响 db-demo 进度。
12.2 Django Model 基础(独立示例)
catalog/models.py(django-style,与 shop_db 对齐):
# django-style — 需 Django 环境;只读学员可跳过运行
from django.db import models
class Category(models.Model):
name = models.CharField("分类名", max_length=64)
slug = models.SlugField(max_length=64, unique=True)
sort_order = models.IntegerField(default=0)
class Meta:
db_table = "categories" # 与 db-demo DDL 表名一致
ordering = ["sort_order", "id"]
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name="products",
)
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=64, unique=True)
description = models.TextField(blank=True)
# Django 常用 Decimal 存「元」;与 db-demo「分」二选一,见 §12.3
price_cents = models.PositiveIntegerField("单价(分)", default=0)
stock = models.PositiveIntegerField(default=0)
is_published = models.BooleanField("是否上架", default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "products"
indexes = [
models.Index(fields=["category", "is_published", "-created_at"]),
]
@property
def price_yuan(self) -> str:
return f"¥{self.price_cents / 100:.2f}"
| Django 字段 | db-demo / SQLAlchemy | 说明 |
|---|---|---|
SlugField | slug: Mapped[str] | URL 标识 |
BooleanField | is_published: Mapped[bool] | 上架开关 |
PositiveIntegerField | price: Mapped[int] | 分 |
ForeignKey | relationship + FK | 多对一 |
Meta.db_table | __tablename__ | 显式表名 |
12.3 价格:分 vs 元(全栈约定)
| 方案 | Django | SQLAlchemy | 优劣 |
|---|---|---|---|
| 整数分(db-demo 标准) | PositiveIntegerField | Mapped[int] | 无浮点误差,支付友好 |
| 小数元(django-web 早期) | DecimalField(10,2) | Numeric(10,2) | 直观,需 Decimal 运算 |
django-web ch07 提示:Web 模块 Product 常用 DecimalField 存元;python-database 与 shop-db 毕业项目 统一 分。对照换算:
# django-style
from decimal import Decimal
yuan = Decimal("59.90")
cents = int(yuan * 100) # 5990
# SQLAlchemy / db-demo
product.price = 5990
12.4 migrate vs Alembic
| 步骤 | Django | Alembic(ch11) |
|---|---|---|
| 检测模型变更 | makemigrations | revision --autogenerate |
| 应用 | migrate | upgrade head |
| 回滚 | migrate app zero | downgrade -1 |
| 版本记录 | django_migrations 表 | alembic_version 表 |
| 脚本位置 | app/migrations/ | alembic/versions/ |
# django-style
python manage.py makemigrations catalog --name evolve_product_fields
python manage.py migrate
django-web ch07 字段演进(与 ch07 对照表呼应):
| 旧字段 | 新字段 | 迁移要点 |
|---|---|---|
sku | slug | RenameField |
is_active | is_published | RenameField |
| — | description | AddField |
| — | updated_at | AddField |
Django 迁移片段(独立阅读):
# django-style migrations/0002_evolve_product_fields.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("catalog", "0001_initial")]
operations = [
migrations.RenameField(model_name="product", old_name="sku", new_name="slug"),
migrations.RenameField(model_name="product", old_name="is_active", new_name="is_published"),
migrations.AddField(model_name="product", name="description", field=models.TextField(blank=True)),
migrations.AddField(model_name="product", name="updated_at", field=models.DateTimeField(auto_now=True)),
]
Alembic 等价见 ch11 §11.7。
12.5 QuerySet 基础
Django ORM 查询入口:Model.objects 返回 QuerySet(惰性,链式)。
# django-style
# 全表(慎用)
Product.objects.all()
# 过滤 — 等价 SQL WHERE
Product.objects.filter(is_published=True)
Product.objects.filter(category__slug="digital", is_published=True)
# 排除
Product.objects.exclude(stock=0)
# 排序
Product.objects.filter(is_published=True).order_by("-created_at")
Product.objects.filter(is_published=True).order_by("price_cents")
# 切片分页 — LIMIT/OFFSET
Product.objects.filter(is_published=True)[0:20]
# 单条
Product.objects.get(slug="wireless-mouse") # 不存在 → DoesNotExist
Product.objects.filter(slug="x").first() # 不存在 → None
# 计数
Product.objects.filter(is_published=True).count()
# 值列表 / 字典
Product.objects.filter(is_published=True).values("slug", "price_cents")
Product.objects.filter(is_published=True).values_list("slug", flat=True)
SQLAlchemy 对照:
# db-demo / SQLAlchemy 2.0
from sqlalchemy import select
stmt = (
select(Product)
.where(Product.is_published.is_(True))
.where(Product.category.has(Category.slug == "digital"))
.order_by(Product.price.asc())
.limit(20)
)
products = session.scalars(stmt).all()
12.6 F() 与 Q() 表达式
12.6.1 F() — 引用字段值
# django-style
from django.db.models import F
# 原子库存递减(django-web ch05)
Product.objects.filter(pk=1).update(stock=F("stock") - 1)
# 同表字段比较
Product.objects.filter(stock__lte=F("price_cents")) # 教学举例
# Order 总额 annotate
from django.db.models import Sum
Order.objects.annotate(
computed_total=Sum(F("lines__quantity") * F("lines__unit_price"))
)
| F() 用途 | SQL 层效果 |
|---|---|
update(stock=F("stock")-1) | SET stock = stock - 1 |
| 避免读-改-写竞态 | 单条 UPDATE 原子 |
SQLAlchemy 对照:
from sqlalchemy import update
stmt = (
update(Product)
.where(Product.id == 1, Product.stock >= 1)
.values(stock=Product.stock - 1)
)
session.execute(stmt)