sql注入在orm中基本消失是因为主流orm默认使用参数化查询,用户输入作为预编译参数传入而非拼接进sql字符串;一旦手动拼接sql、动态拼表名列名或绕过orm抽象层,风险即回归,须用白名单校验等安全措施。

SQLAlchemy、Django ORM、Peewee 这类成熟 ORM 不是“为了抽象而抽象”,而是解决手写 SQL 字符串时真实存在的硬伤。直接拼接字符串看似自由,但多数人在上线前就踩过坑。
SQL注入漏洞不是理论风险,而是默认行为
只要用 + 或 f-string 拼接用户输入进 SQL,就等于把数据库钥匙交给调用方。
常见错误写法:"SELECT * FROM book WHERE author = '" + request.args.get('author') + "'"
- 哪怕加了
strip()或正则过滤,也挡不住绕过技巧(比如 Unicode 空格、注释符--) -
cursor.execute("SELECT * FROM user WHERE id = %s", [user_id])是安全的,但前提是所有查询都严格走参数化——而人会忘、会抄错、会为“方便”绕开 - ORM 的
filter()、where()等方法默认强制参数化,不给你留拼接机会
跨数据库迁移成本远高于预期
你写的 SELECT ... LIMIT 10 OFFSET 20 在 PostgreSQL 可用,在 MySQL 8.0+ 也行,但在 SQLite 或旧版 MySQL 就得改成 LIMIT 20, 10;JSON_EXTRACT、GENERATE_SERIES 这类函数更是完全不可移植。
ORM 的查询构建器(如 query.filter(Book.author == author))输出的底层 SQL 会根据 dialect 自动适配。
- 换数据库只需改连接串和少数方言配置,不用逐行检查 SQL
- 测试环境用 SQLite、生产用 PostgreSQL 时,
order_by()和distinct()行为基本一致 - 手写 SQL 里混用
COALESCE和IFNULL这类函数,等于提前锁死数据库选型
对象映射不是“多一层封装”,而是省掉重复 glue code
每次 cursor.fetchall() 返回的是 tuple 或 dict,你要手动 new 对象、赋值字段、处理空值、转换时间类型——这些代码在每个 DAO 层里反复出现。
ORM 的 Book.query.filter(...).first() 直接返回带属性、关系、验证逻辑的 Book 实例。
- 字段名变更时,只改 model 定义,不用搜遍所有
SELECT *和row[3] -
book.author.name自动触发 JOIN 或懒加载,不用手写关联子查询 - 更新时只设
book.price = 99.9,ORM 自动对比 dirty fields,生成最小 UPDATE 语句
性能问题常被误归因,实际可控
有人觉得 ORM “太重”“生成 SQL 效率低”,但真实瓶颈往往不在 ORM 层。
- 90% 的慢查询来自没加索引、N+1 查询(比如循环中查关联表),ORM 提供
select_related()、joinedload()明确控制加载策略 -
Query().options(lazyload('*'))或session.expire_on_commit = False可关闭自动刷新,避免隐式查询 - 真有性能 critical 场景(如报表导出),ORM 允许退回到原生 SQL:
session.execute(text("...")),不破坏整体结构
手写 SQL 并非禁忌,但它适合明确知道代价的场景:已知 DB 特性、查询固定、无用户输入、且团队能统一维护 SQL 脚本。日常业务逻辑里,用成熟 ORM 是用约定压制不确定性——不是放弃控制权,而是把精力花在业务上,而不是反复对抗 SQL 字符串的脆弱性。











