SQLAlchemy 的两套 API

文章来源声明: 原文作者:cxs; 来源站点:掘金; 原文链接:https://juejin.cn/post/7689004808923349032; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

一篇把 ORM 与 Core 边界讲透的科普。适合正在读或改 RuoYi-Python 数据层、对「表.c.字段」「返回 Row」「分页挂 flask.g」感到困惑的开发者。

什么是"纯 SQLAlchemy Core 风格(非 ORM Session 查询)" -------------------------------------------

你贴的这段话是在说:这两个 Mapper 类没有用 SQLAlchemy 的 ORM 方式(定义 class User(Base) 然后 session.query(User).filter(...)),而是用了 SQLAlchemy Core 的方式——直接拼 SQL 表达式、直接执行。

一、先分清 SQLAlchemy 的两套 API

SQLAlchemy 其实有两套独立但可混用的体系:

**SQLAlchemy Core****SQLAlchemy ORM**
核心对象`Table`、`Column`、`select()`、`insert()`、`update()`、`delete()``Base`、`class User(Base)`、`session.query()`、`session.add()`
思维模式**面向 SQL**:你写的是 SQL 表达式,Python 帮你拼成 SQL**面向对象**:你操作的是 Python 对象,ORM 帮你翻译成 SQL
结果返回 `Row`(类似 namedtuple)返回**对象实例**(User 对象)
状态管理无 session 状态跟踪有 identity map、脏检查、级联、延迟加载
关系手写 join`relationship()`、`backref` 自动处理
典型代码`conn.execute(select(user_table).where(user_table.c.id == 1))``session.query(User).filter(User.id == 1).first()`

注意: Core 是 ORM 的底层。ORM 内部就是靠 Core 把对象操作翻译成 SQL 的。所以"用 Core"不是"绕过 SQLAlchemy",而是用了更底层、更贴近 SQL 的那一层。

二、两种风格代码对比

假设有张表 bsv_meta:

ORM 风格(Session 查询)

<span># 1. 定义模型</span>
<span>class</span> <span>BsvMeta</span>(<span>Base</span>):
    __tablename__ = <span>"bsv_meta"</span>
    <span>id</span> = Column(Integer, primary_key=<span>True</span>)
    name = Column(String(<span>64</span>))
    enabled = Column(Boolean, default=<span>False</span>)

<span># 2. 查询</span>
<span>def</span> <span>get_meta</span>(<span>meta_id: <span>int</span></span>):
    <span>return</span> db.session.query(BsvMeta).<span>filter</span>(BsvMeta.<span>id</span> == meta_id).first()
    <span># 返回 BsvMeta 对象,可以 meta.name、meta.enabled 访问</span>

<span># 3. 插入</span>
<span>def</span> <span>create_meta</span>(<span>name: <span>str</span></span>):
    m = BsvMeta(name=name)
    db.session.add(m)
    db.session.commit()
    <span>return</span> m

Core 风格(非 ORM Session 查询)

<span># 1. 定义表(不是类!)</span>
bsv_meta = Table(
    <span>"bsv_meta"</span>, metadata,
    Column(<span>"id"</span>, Integer, primary_key=<span>True</span>),
    Column(<span>"name"</span>, String(<span>64</span>)),
    Column(<span>"enabled"</span>, Boolean, default=<span>False</span>),
)

<span># 2. 查询</span>
<span>def</span> <span>get_meta</span>(<span>meta_id: <span>int</span></span>):
    stmt = select(bsv_meta).where(bsv_meta.c.<span>id</span> == meta_id)
    row = db.session.execute(stmt).first()
    <span># 返回 Row,用 row.id / row.name 或 row._mapping["name"] 访问</span>

<span># 3. 插入</span>
<span>def</span> <span>create_meta</span>(<span>name: <span>str</span></span>):
    stmt = insert(bsv_meta).values(name=name)
    result = db.session.execute(stmt)
    db.session.commit()
    <span>return</span> result.inserted_primary_key

关键区别:

  • Core 里表是 Table 对象,字段通过 bsv_meta.c.id(.c = columns)访问。
  • 查询用 select(...)、insert(...) 这种函数式构造 SQL 语句,再 execute()。
  • 返回的是 Row(像带字段名的元组),不是 ORM 对象。

三、"非 ORM Session 查询"到底强调什么

这句话里的"非 ORM Session 查询"是在强调:

它虽然用了 db.session.execute()(借用了 session 来执行),但执行的是 Core 的 select() 语句,而不是 ORM 的 session.query(Model)。

session 在这里只是**"执行器 + 事务上下文"**,不承担 ORM 的"对象状态跟踪"职责。也就是说:

<span># ORM 查询:session 会跟踪返回的对象、缓存、脏检查</span>
session.query(User).<span>filter</span>(...).<span>all</span>()

<span># Core 查询:session 只是执行 SQL,返回原始 Row,不跟踪对象</span>
session.execute(select(user_table).where(...)).<span>all</span>()

同一个 session,两种用法。 第二种就是"非 ORM Session 查询"。

四、为什么 RuoYi-Python 的 Mapper 要用 Core 风格

结合你贴的那段描述(571 行的 CRUD + 互斥启用 + 引用计数 + 分页拦截),用 Core 有几个现实理由:

理由说明
**贴近若依框架原设计**RuoYi 的 Mapper 层本来就是"写 SQL / 映射 SQL"的思路(Java 版是 XML 写 SQL)。Core 风格最接近这种"我在写 SQL"的心智模型。
**复杂 SQL 更好控制**互斥启用(`UPDATE ... SET enabled=0 WHERE type=X`)、引用计数(子查询 `COUNT(*)`)、时间范围筛选,用 Core 拼比 ORM 的 `relationship` 更直观、更可控。
**不想建 ORM 模型**元数据表往往字段多、结构固定、不需要对象关系,定义 6 个 ORM Model 反而累赘。直接 `Table` + `select` 更轻。
**性能可控**Core 少一层 ORM 对象构造和状态跟踪,批量操作、复杂 join 时开销更小、行为更可预测。
**分页拦截器**你提到的 `flask.g.criterian_meta.page.stmt` 注入若依分页——分页拦截器通常要**拿到原始 `select` 语句**去改写成 `COUNT` 和 `LIMIT`。Core 的 `select()` 语句对象天生适合被拦截改写;ORM 的 `session.query()` 也能做,但 Core 更直接。

五、一句话总结

"纯 SQLAlchemy Core 风格(非 ORM Session 查询)"= 用 Table + select()/insert()/update()/delete() 来构造和执行 SQL,返回 Row;而不是用 class Xxx(Base) + session.query(Xxx) 返回 ORM 对象。 session 只当执行器和事务用,不参与对象状态管理。

六、对你的实际影响

如果你要读懂或修改 BsvMetaMapper / MeasureRecordMapper,记住:

  1. 里面出现的字段访问是 表.c.字段(如 bsv_meta.c.name),不是 对象.字段。
  2. 返回值是 Row,用 row.name 或 row._mapping["name"] 取值;不能 row.name = "x" 改完就自动同步(那是 ORM 才有)。
  3. 想看它到底执行了什么 SQL,可以 print(stmt.compile(compile_kwargs={"literal_binds": True}))。
  4. 分页不靠 LIMIT 手写,而是把 stmt 挂到 flask.g.criterian_meta.page.stmt,由若依的拦截器统一改写。

如果你想,可以把 BsvMetaMapper 里一个具体方法贴出来,我帮你逐行翻译成它实际执行的 SQL,你就能彻底对上号。