Django ORM 优缺点分析
1. 概述
Django ORM(Object-Relational Mapper)是 Django 框架内置的对象关系映射层。开发者用 Python 类描述表结构,用 QuerySet 描述查询,由 ORM 生成对应数据库的 SQL。
它不是一个独立的 ORM 库,而是和 Django 的模型、迁移、Admin、表单校验、事务、信号体系绑在一起。因此评价它时,不能只看「能不能写 SQL」,还要看它在整个 Django 生态里带来的开发效率与约束。
一句话判断:
- 适合:业务表结构清晰、CRUD 为主、需要快速交付、多数据库方言可切换的 Web 应用。
- 不适合:复杂分析 SQL、强依赖存储过程/触发器、与 ORM 模型严重不一致的遗留库、超大规模批量写入。
2. 核心能力速览
| 能力 | 说明 | 常用入口 |
|---|---|---|
| 模型定义 | 用 Python 类映射表、字段、约束、关系 | models.Model |
| 查询构建 | 惰性、可链式组合的查询对象 | Model.objects.filter() |
| 关系加载 | 减少 N+1 查询 | select_related / prefetch_related |
| 聚合统计 | COUNT / SUM / AVG / 分组 |
aggregate / annotate |
| 批量写 | 少次数往返插入/更新/删除 | bulk_create / bulk_update / update / delete |
| 事务 | 原子提交、保存点 | transaction.atomic() |
| 迁移 | 模型变更生成 SQL 并版本化 | makemigrations / migrate |
| 多库 | 同一套模型切不同后端 | PostgreSQL / MySQL / SQLite / Oracle |
| 逃生舱 | 必要时写原生 SQL | raw() / extra() / connection.cursor() |
3. 优点
3.1 开发效率高,API 统一且直观
常见增删改查可以用接近自然语言的 Python 表达,学习曲线相对平缓,团队上手快。
from django.db.models import Q
# 创建
order = Order.objects.create(order_no="SO20260813001", state=20)
# 查询:链式、可读
qs = (
Order.objects
.filter(state__in=[20, 21, 22], is_enable=True)
.exclude(Q(remark="") | Q(remark__isnull=True))
.order_by("-create_time")
)
# 更新 / 删除(QuerySet 级,一条 SQL)
Order.objects.filter(id=order.id).update(state=25)
Order.objects.filter(state=0).delete()
好处:
- 少写样板 SQL,业务代码更聚焦领域逻辑。
- 字段查找语法统一:
exact、in、range、icontains、isnull、跨表__等。 - IDE 补全、类型提示、Code Review 成本低于散落的 SQL 字符串。
3.2 QuerySet 惰性求值,方便组合复用
QuerySet 在求值前不会打数据库。过滤、排序、切片可以层层拼装,适合封装公共查询。
def enabled_orders():
return Order.objects.filter(is_enable=True)
def warehouse_out_orders(user_id: int):
return (
enabled_orders()
.filter(user_id=user_id, order_type=20)
.select_related("user")
)
# 上面两段都还没查库;直到迭代、list()、count()、exists() 才执行
page = warehouse_out_orders(1001)[:20]
配套特性:
- 结果缓存:同一
QuerySet第一次求值后会缓存行,避免重复查询。 - 切片转
LIMIT/OFFSET:qs[10:20]在数据库层分页,而不是先拉全表。 - 短路方法:
exists()、count()、first()比len(list(qs))更省。
3.3 跨数据库抽象做得扎实
同一套模型可以跑在 PostgreSQL、MySQL / MariaDB、SQLite、Oracle 上。开发用 SQLite、测试/生产切 MySQL 或 PostgreSQL 是常见做法。
价值:
- 减少「为每个数据库手写方言」的成本。
- 字段类型、约束、部分函数由后端适配器处理。
- 对中小型项目,迁移数据库的阻力明显小于纯手写 SQL。
注意:抽象不是 100%。窗口函数、JSON 操作、upsert、全文检索等仍有后端差异,关键路径要按目标库验证。
3.4 关系声明清晰,关联查询有现成优化手段
ForeignKey、OneToOneField、ManyToManyField 把表关系写进模型,反向关系、级联删除、on_delete 策略都显式可见。
class User(models.Model):
username = models.CharField(max_length=64)
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="orders")
amount = models.DecimalField(max_digits=12, decimal_places=2)
# 一对多正向:JOIN 一次取出用户
orders = Order.objects.select_related("user").filter(amount__gte=100)
# 一对多反向:额外查询后按主键拼装,避免循环里再查
users = User.objects.prefetch_related("orders")
| 方法 | SQL 形态 | 适用关系 | 特点 |
|---|---|---|---|
select_related |
JOIN 一次查出 |
正向 FK / OneToOne | 查询次数少,结果行可能变宽 |
prefetch_related |
拆成多次查询再组装 | 反向 FK / M2M | 适合一对多、多对多 |
Prefetch(...) |
可给预取加过滤/排序 | 复杂预取 | 避免把无关子表整表拉回 |
对常规业务列表页(订单带用户、单据带明细摘要),这两套 API 已经能覆盖大多数 N+1 问题。
3.5 迁移系统成熟,结构变更可版本化
makemigrations / migrate 把模型变更落成可审查、可回滚的迁移文件,适合多人协作。
常见能力:
- 增删字段、改默认值、加索引/约束。
- 数据迁移(
RunPython/RunSQL)和结构迁移放在同一条时间线。 - 多 app 依赖关系明确,CI 里可以跑
migrate --check或检测冲突。
这是 Django 相对「只改模型、不管库」方案的显著优势:模型和数据库有正式的同步机制。
3.6 校验、约束、信号、Admin 形成闭环
ORM 不只是查询器,还带一层业务护栏:
- 字段级约束:
unique、null、blank、choices、validators。 - 模型级约束:
UniqueConstraint、CheckConstraint、Meta.constraints。 full_clean()/ ModelForm 在入库前做校验。pre_save/post_save等信号便于审计、缓存失效、联动写。- Admin 基于模型自动生成后台,对内部运营工具很省时间。
对中后台、内容管理、权限用户体系,这个闭环能显著缩短交付周期。
3.7 事务与一致性 API 完整
from django.db import transaction
@transaction.atomic
def create_out_order(payload):
order = Order.objects.create(**payload["order"])
OrderDetail.objects.bulk_create([
OrderDetail(order=order, **row) for row in payload["details"]
])
# 任一步异常,整段回滚
补充能力:
- 保存点:嵌套
atomic()。 - 提交后回调:
transaction.on_commit(lambda: send_msg()),避免事务未提交就发消息。 - 指定数据库:
atomic(using="warehouse")。 - 行锁:
select_for_update(),处理并发扣库存、抢单等场景。
3.8 查询表达力对「常规业务 SQL」足够
除了 CRUD,Django ORM 也能写不少中等复杂度的查询:
from django.db.models import Count, Sum, F, Value, Window
from django.db.models.functions import Rank
# 分组聚合
stats = (
Order.objects
.filter(order_type=20)
.values("user_id")
.annotate(order_count=Count("id"), total_qty=Sum("quantity"))
.order_by("-total_qty")
)
# 表达式更新,避免先读后写
Order.objects.filter(id=1).update(quantity=F("quantity") + 1)
# 窗口函数(Django 2.0+)
ranked = Order.objects.annotate(
rk=Window(expression=Rank(), partition_by=[F("user_id")], order_by=F("amount").desc())
)
另外还有:
Q对象组合复杂 AND/OR。FilteredRelation、Subquery/OuterRef做相关子查询。JSONField查询(尤其 PostgreSQL)。GeneratedField(Django 5.0+)映射数据库生成列。- 异步接口:
aget()、afilter()、acreate()等(Django 4.1+)。
3.9 生态完整,工程约定清楚
- 官方文档质量高,版本说明清楚。
- 测试:
TestCase默认包事务,数据库测试好写。 - 调试:
qs.query、django.db.connection.queries、django-debug-toolbar。 - 社区方案多:
django-filter、django-cte、django-mysql等可补缺口。
4. 缺点
4.1 容易写出 N+1,性能「看起来能跑、量上来就炸」
默认按需加载关联对象。模板或循环里访问 order.user.name,每一行都可能再打一次库。
# 危险:100 条订单 ≈ 1 + 100 次查询
for order in Order.objects.filter(state=20)[:100]:
print(order.user.username)
相关坑:
- 忘记
select_related/prefetch_related。 prefetch_related后再对子表filter(),可能打掉缓存,重新查询。count()、exists()、切片、迭代混用,同一逻辑打多次 SQL。list(qs)后再qs.count(),缓存帮不上COUNT(*)。
ORM 提供了优化手段,但不会自动替你优化。列表接口、报表、树形关系是重灾区。
4.2 复杂 SQL 表达力有上限
窗口函数、简单子查询可以写;以下场景通常会别扭甚至写不出来:
- 多层 CTE / 递归 CTE(闭包表、无限级组织树)。
LATERAL JOIN、复杂UNION分页。- 数据库特定优化器 hint、分区裁剪、索引提示。
- 存储过程、触发器、自定义聚合的深度编排。
- 一份 SQL 里多段临时结果复用。
常见后果:
- 代码被拆成多次查询,在 Python 里拼结果,又慢又容易不一致。
- 最终还是
raw()/cursor.execute(),但模型层和 SQL 层两套逻辑要一起维护。
递归 CTE 需要 django-cte 或原生 SQL,不是开箱即用。
4.3 批量写有「静默丢行为」
bulk_create、bulk_update、QuerySet.update()、QuerySet.delete() 为了速度会跳过一部分模型机制。
| 操作 | 不触发 / 不执行的内容 |
|---|---|
bulk_create |
save()、pre_save / post_save、逐行校验 |
bulk_update |
同上,且必须手动指定更新字段 |
QuerySet.update() |
save()、auto_now 是否生效取决于版本与写法、信号 |
QuerySet.delete() |
实例级 delete() 逻辑;大表删除仍可能很慢 |
其他限制(以官方文档为准,不同版本略有差异):
- 多表继承的子模型不能直接
bulk_create。 - M2M 关系不会被批量插入带着处理。
- 自增主键回填:PostgreSQL / MariaDB / 高版本 SQLite 支持较好,MySQL 历史上较弱。
ignore_conflicts/update_conflicts(upsert)依赖后端,语义不完全一致。- 单次插入行数受后端参数限制,必须自己
batch_size。
如果审计、缓存失效、冗余字段同步写在 save() 或信号里,批量接口会「成功但副作用没发生」。这是线上事故高发点。
4.4 运行时开销与内存占用高于手写 SQL
每次查询大致经历:拼表达式 → 编译 SQL → 驱动取数 → 把每一行水合成 Model 实例。
影响:
- 高 QPS 的简单主键查询,ORM 对象创建成本不可忽略。
qs.all()不切片会把结果集一次性装进内存。- 宽表 +
select_related多层 JOIN,行膨胀,内存和网络都差。 - 大结果集应使用
iterator(chunk_size=2000),否则可能把 worker 打满。
# 导出、巡检:流式迭代,降低峰值内存
for row in BarCode.objects.filter(is_enable=1).iterator(chunk_size=2000):
process(row)
只需要少量列时,用 values() / values_list() / only() / defer(),不要默认拉整模型。
4.5 与真实库结构强耦合,遗留库 / 脏库很难用
ORM 假设「模型是表结构的真相」。下面情况会很痛:
- 老库字段名、类型、空值、默认值和模型不一致。
- 表没有主键、复合主键历史包袱(Django 5.2 才正式支持
CompositePrimaryKey,老项目仍常见 workaround)。 - 同一张表被触发器、作业、其他语言服务同时改。
managed = False映射外部表后,迁移帮不上忙,字段稍有偏差就报错或写坏数据。
模型与库不一致时,轻则查询报错,重则写入落错列。对「先有库、后有模型」或「多套系统共库」的场景,原生 SQL 往往更安全。
4.6 查询语义有隐蔽陷阱
这些不是 bug,但非常容易用错:
-
filter对 M2M / 反向关系可能产生重复行
需要distinct(),而distinct+order_by在部分后端有额外限制。 -
default只作用于 ORM 创建路径
原生 SQL、别的服务插入时不会带上模型default;库表本身若无默认值,就会落NULL。 -
auto_now/auto_now_add与update()
QuerySet.update()默认不会刷新auto_now字段,需要显式写入。 -
时区
USE_TZ=True时,DateTimeField的朴素/感知时间转换容易错。 -
null=Truevsblank=True
一个管数据库,一个管表单校验,混用会导致空字符串和NULL并存,查询漏数据。 -
切片后不能再
filter/order_by
已求值或已切片的 QuerySet 限制比看起来更严。 -
get()多行会抛MultipleObjectsReturned
数据不唯一时,接口会直接 500,需要业务侧保证约束或改用filter().first()。
4.7 异步支持仍是「能用,但不像同步那么完整」
Django 4.1+ 提供 aget / afilter / asave 等异步方法,但:
- 同步 ORM 调用不能直接放进 ASGI 异步视图(会阻塞事件循环,开发模式会报警)。
- 事务、部分第三方库、信号接收器仍偏同步。
- 连接池、线程/异步上下文切换要比同步 WSGI 更小心。
如果项目主体已经是 FastAPI + 异步 SQLAlchemy / 原生驱动,为了 ORM 再引入 Django 通常不划算。
4.8 调试生成 SQL 的成本高于手写 SQL
链式调用很长时,真正执行的 SQL 可能和直觉不一致:
- 重复 JOIN、错误的
GROUP BY、隐式子查询。 annotate+filter的顺序会改变 SQL(WHEREvsHAVING)。- 不同后端对
NULL排序、GROUP BY宽松度不同。
必须养成看 str(qs.query) 和慢查询日志的习惯。ORM 降低了「写 SQL」的门槛,但没有降低「懂 SQL」的门槛。
4.9 测试与迁移在中后期会变重
- 迁移文件随时间膨胀,合并冲突常见。
- 错误的数据迁移上线后回滚困难。
TestCase包事务很快,但用TransactionTestCase、多库、原生 DDL 时测试变慢。squashmigrations需要额外治理,很多团队一直拖着。
5. 和其他方案对比
| 维度 | Django ORM | SQLAlchemy 2.x | 原生 SQL |
|---|---|---|---|
| 学习成本 | 低,API 意见性强 | 中高,Core / ORM 两层 | 低入门、高精通 |
| 与 Web 框架 | 和 Django 深度绑定 | 框架无关 | 框架无关 |
| 复杂查询 | 中等,复杂场景要逃生 | 强,Core 几乎能写任意 SQL | 最强 |
| 迁移 | 内置且成熟 | Alembic,需单独集成 | 自建 |
| 批量与性能可控性 | 中,需避开陷阱 | 较好 | 最好 |
| 遗留库 / 脏 schema | 弱 | 较强 | 最灵活 |
| 后台 / Admin | 开箱即用 | 无 | 无 |
| 适合团队 | Django 全栈、中后台 | 服务化、复杂域模型 | 报表、同步、修复脚本 |
补充:
- Peewee / Tortoise / SQLModel:更轻,但生态和迁移完整度一般不如 Django。
- Django ORM 的优势不在「SQL 能力最强」,而在「和 Django 项目咬合最紧」。
6. 适用与不适用
6.1 适合用 Django ORM
- 标准 CRUD、审批流、订单头行结构、权限用户。
- 表结构由本系统主导,能接受迁移约束。
- 需要 Admin、表单校验、信号审计一起上。
- 查询以等值、范围、外键关联、简单聚合为主。
- 团队更熟 Python,不希望满项目拼接 SQL。
6.2 不适合硬套 ORM
- 复杂报表、多表窗口分析、递归组织树。
- 一次写入十万、百万级条码 / 流水,必须吃满数据库批量能力。
- 遗留库与模型字段对不齐,或存在大量触发器、跨库同步。
- 热点行并发更新对 SQL 执行计划极度敏感。
- 已有 FastAPI / 自研框架,仅为了 ORM 引入整个 Django。
6.3 推荐的混合策略
不要二元选择「全 ORM」或「全 SQL」:
- 日常 CRUD 走 ORM,保证可读性和约束。
- 列表关联必须写
select_related/prefetch_related,并在 Review 里检查。 - 批量导入、对账、修复脚本走
bulk_*或cursor.executemany,不要循环save()。 - 复杂报表、递归查询、跨库对账走原生 SQL,结果再映射成 DTO,不必强行水合成 Model。
- 关键路径看执行计划,给过滤字段建索引;ORM 不会自动建对业务索引。
7. 实践建议
7.1 查询
- 先想清楚要哪些列、哪些关联,再写 QuerySet。
- 循环访问关联前,先补
select_related/prefetch_related。 - 只要统计不要实体:用
count()/aggregate()/values()。 - 大结果集:
iterator();分页:数据库层切片,不要list(qs)后再切。 - 用
exists()判断存在,不要if qs:或len(qs)。
7.2 写入
- 单条、需要校验和信号:
save()/create()。 - 批量:
bulk_create(..., batch_size=500),并确认不依赖信号。 - 批量更新:
update()或bulk_update,显式更新update_time。 - 并发库存:
select_for_update()+F()表达式,避免先读后写丢更新。 - 事务里发 MQ / 打外部接口:放到
on_commit。
7.3 模型与迁移
- 数据库约束和模型约束一起建,不要只写在 Python 里。
- 空值策略统一:业务空用
NULL还是空字符串,全项目约定死。 - 删除优先软删字段;硬删要有备份表或审计日志。
- 迁移必须可审查;数据迁移与结构迁移分开更安全。
- 对外部遗留表设
managed = False,并单独验证字段映射。
7.4 可观测性
- 开发环境打开查询日志或 Debug Toolbar。
- 慢接口打印
str(qs.query),对照EXPLAIN。 - 给高频过滤列、关联列建索引;避免在无索引大字段上
LIKE '%xx%'。 - 监控 N+1:同一请求查询次数突然变多,优先查循环里的关联访问。
8. 优缺点对照表
| 优点 | 对应的代价 |
|---|---|
| 写起来快、代码短 | 复杂 SQL 反而更难写、更难调 |
| 惰性 QuerySet 好组合 | 不知道何时真正打库,容易重复查询 |
| 跨数据库方言 | 高级特性仍要按后端验证 |
select_related / prefetch_related |
不用就 N+1,用过头就 JOIN 爆炸 |
| 迁移可版本化 | 中后期冲突和历史迁移包袱重 |
save() / 信号 / Admin 闭环 |
批量接口会跳过这些机制 |
| 事务 API 完整 | 长事务、嵌套 atomic 使用不当会锁表 |
| 和 Django 深度集成 | 脱离 Django 单独使用价值下降 |
9. 结论
Django ORM 的核心价值是 用统一的模型层,把常规业务数据访问、校验、迁移、后台管理收拢到一套约定里。它足够好,好在 80% 的 Web 业务查询都能写得又快又清楚;它不够强,强不在极致 SQL 表达力和极致吞吐。
选型建议:
- 做 Django 项目:默认用 ORM。 把精力放在关系加载、索引、事务和批量接口的正确使用上。
- 遇到报表、递归、超大批量、遗留脏库:主动降级到 SQL。 这是成熟用法,不是「没用好 ORM」。
- 评价标准不要只看语法好不好写。 要看生成 SQL、执行计划、内存、信号副作用、以及模型和真实表是否一致。
对大多数中后台和业务系统,Django ORM 是加分项;对数据分析、同步作业、和模型对不齐的老库,它经常是阻力。用其所长,留好逃生舱,比争论「ORM 过时还是银弹」更有用。
附录:常见 API 速查
# 查
Model.objects.get(pk=1) # 单条,没有或不唯一会抛异常
Model.objects.filter(...).first() # 单条或 None
Model.objects.filter(...).exists() # 是否存在
Model.objects.filter(...).count() # COUNT(*)
Model.objects.filter(...).values("id") # 字典列表
Model.objects.filter(...).only("id") # 延迟加载其余字段
# 关联
qs.select_related("user", "warehouse")
qs.prefetch_related("details", "details__sku")
# 写
obj.save()
Model.objects.create(...)
Model.objects.bulk_create(objs, batch_size=500)
Model.objects.filter(pk=1).update(state=20)
Model.objects.bulk_update(objs, ["state", "update_time"])
# 事务与锁
from django.db import transaction
with transaction.atomic():
obj = Model.objects.select_for_update().get(pk=1)
- 点赞
- 收藏
- 关注作者
评论(0)