Django ORM 优缺点分析

举报
GrayParis 发表于 2026/08/13 11:47:40 2026/08/13
【摘要】 1. 概述Django ORM(Object-Relational Mapper)是 Django 框架内置的对象关系映射层。开发者用 Python 类描述表结构,用 QuerySet 描述查询,由 ORM 生成对应数据库的 SQL。它不是一个独立的 ORM 库,而是和 Django 的模型、迁移、Admin、表单校验、事务、信号体系绑在一起。因此评价它时,不能只看「能不能写 SQL」,还...

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,业务代码更聚焦领域逻辑。
  • 字段查找语法统一:exactinrangeicontainsisnull、跨表 __ 等。
  • 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/OFFSETqs[10:20] 在数据库层分页,而不是先拉全表。
  • 短路方法exists()count()first()len(list(qs)) 更省。

3.3 跨数据库抽象做得扎实

同一套模型可以跑在 PostgreSQL、MySQL / MariaDB、SQLite、Oracle 上。开发用 SQLite、测试/生产切 MySQL 或 PostgreSQL 是常见做法。

价值:

  • 减少「为每个数据库手写方言」的成本。
  • 字段类型、约束、部分函数由后端适配器处理。
  • 对中小型项目,迁移数据库的阻力明显小于纯手写 SQL。

注意:抽象不是 100%。窗口函数、JSON 操作、upsert、全文检索等仍有后端差异,关键路径要按目标库验证。

3.4 关系声明清晰,关联查询有现成优化手段

ForeignKeyOneToOneFieldManyToManyField 把表关系写进模型,反向关系、级联删除、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 不只是查询器,还带一层业务护栏:

  • 字段级约束:uniquenullblankchoicesvalidators
  • 模型级约束:UniqueConstraintCheckConstraintMeta.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。
  • FilteredRelationSubquery / OuterRef 做相关子查询。
  • JSONField 查询(尤其 PostgreSQL)。
  • GeneratedField(Django 5.0+)映射数据库生成列。
  • 异步接口:aget()afilter()acreate() 等(Django 4.1+)。

3.9 生态完整,工程约定清楚

  • 官方文档质量高,版本说明清楚。
  • 测试:TestCase 默认包事务,数据库测试好写。
  • 调试:qs.querydjango.db.connection.queriesdjango-debug-toolbar
  • 社区方案多:django-filterdjango-ctedjango-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_createbulk_updateQuerySet.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,但非常容易用错:

  1. filter 对 M2M / 反向关系可能产生重复行
    需要 distinct(),而 distinct + order_by 在部分后端有额外限制。

  2. default 只作用于 ORM 创建路径
    原生 SQL、别的服务插入时不会带上模型 default;库表本身若无默认值,就会落 NULL

  3. auto_now / auto_now_addupdate()
    QuerySet.update() 默认不会刷新 auto_now 字段,需要显式写入。

  4. 时区
    USE_TZ=True 时,DateTimeField 的朴素/感知时间转换容易错。

  5. null=True vs blank=True
    一个管数据库,一个管表单校验,混用会导致空字符串和 NULL 并存,查询漏数据。

  6. 切片后不能再 filter / order_by
    已求值或已切片的 QuerySet 限制比看起来更严。

  7. 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(WHERE vs HAVING)。
  • 不同后端对 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」:

  1. 日常 CRUD 走 ORM,保证可读性和约束。
  2. 列表关联必须写 select_related / prefetch_related,并在 Review 里检查。
  3. 批量导入、对账、修复脚本走 bulk_*cursor.executemany,不要循环 save()
  4. 复杂报表、递归查询、跨库对账走原生 SQL,结果再映射成 DTO,不必强行水合成 Model。
  5. 关键路径看执行计划,给过滤字段建索引;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 表达力和极致吞吐。

选型建议:

  1. 做 Django 项目:默认用 ORM。 把精力放在关系加载、索引、事务和批量接口的正确使用上。
  2. 遇到报表、递归、超大批量、遗留脏库:主动降级到 SQL。 这是成熟用法,不是「没用好 ORM」。
  3. 评价标准不要只看语法好不好写。 要看生成 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)
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。