你贴的这段话是在说:这两个 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,记住:
- 里面出现的字段访问是
表.c.字段(如bsv_meta.c.name),不是对象.字段。 - 返回值是
Row,用row.name或row._mapping["name"]取值;不能row.name = "x"改完就自动同步(那是 ORM 才有)。 - 想看它到底执行了什么 SQL,可以
print(stmt.compile(compile_kwargs={"literal_binds": True}))。 - 分页不靠
LIMIT手写,而是把stmt挂到flask.g.criterian_meta.page.stmt,由若依的拦截器统一改写。
如果你想,可以把 BsvMetaMapper 里一个具体方法贴出来,我帮你逐行翻译成它实际执行的 SQL,你就能彻底对上号。
一篇把 ORM 与 Core 边界讲透的科普。适合正在读或改 RuoYi-Python 数据层、对「表.c.字段」「返回 Row」「分页挂 flask.g」感到困惑的开发者。