<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ORM on Jiangwan&#39;s Blog</title>
        <link>https://jiangwan.ink/tags/orm/</link>
        <description>Recent content in ORM on Jiangwan&#39;s Blog</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sat, 28 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jiangwan.ink/tags/orm/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>SQLAlchemy 核心架构解析</title>
        <link>https://jiangwan.ink/p/sqlalchemy-core-architecture-analysis/</link>
        <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
        
        <guid>https://jiangwan.ink/p/sqlalchemy-core-architecture-analysis/</guid>
        <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;导读&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;很多开发者在使用 SQLAlchemy 时经常遇到诸如 &lt;code&gt;DetachedInstanceError&lt;/code&gt; 或“N+1 查询”等令人头疼的问题。这往往是因为只把它当成了拼接 SQL 的工具，而没有理解其底层的分层架构。本文将从对象与关系的“阻抗失配”出发，拆解 SQLAlchemy 的双层设计哲学，并复盘其状态管理机制中的常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;一-设计目的&#34;&gt;一、 设计目的
&lt;/h2&gt;&lt;p&gt;在后端开发中，我们使用的编程语言（如 Python）是&lt;strong&gt;面向对象&lt;/strong&gt;的，而底层存储（如 MySQL、PostgreSQL）则是&lt;strong&gt;关系型二维表&lt;/strong&gt;。这两者之间存在天然的语义鸿沟：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对象系统&lt;/strong&gt;：有继承、多态，数据表现为图状的网络引用（比如一个 User 对象里面嵌套了一个 List 的 Order 对象）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关系系统&lt;/strong&gt;：只有扁平的行和列，通过外键来表达关联。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种不匹配在软件工程中被称为“&lt;strong&gt;阻抗失配&lt;/strong&gt;” (Object-Relational Impedance Mismatch)。&lt;/p&gt;
&lt;p&gt;SQLAlchemy 存在的目的，并不是简单地帮你省去写 SQL 字符串的麻烦，而是作为一座桥梁，&lt;strong&gt;抹平这种阻抗失配，负责在内存中的对象状态和磁盘上的二维表状态之间进行双向同步。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;二-核心架构&#34;&gt;二、 核心架构
&lt;/h2&gt;&lt;p&gt;为了做好这道桥梁，SQLAlchemy 采用了极其严密的&lt;strong&gt;双层解耦架构&lt;/strong&gt;。&lt;/p&gt;
&lt;pre class=&#34;mermaid&#34;&gt;
  flowchart TD
  subgraph ORMLayer[&amp;#34;ORM 层 (Object Relational Mapper)&amp;#34;]
        Session[Session&amp;lt;br/&amp;gt;会话/工作单元]
        Model[Python Objects&amp;lt;br/&amp;gt;映射对象]
        Session &amp;lt;--&amp;gt; Model
    end

  subgraph CoreLayer[&amp;#34;Core 层 (底层核心)&amp;#34;]
        Engine[Engine&amp;lt;br/&amp;gt;核心引擎]
        Pool[(Connection Pool&amp;lt;br/&amp;gt;连接池)]
        Dialect[Dialect&amp;lt;br/&amp;gt;数据库方言]
        
        Engine --&amp;gt; Pool
        Engine --&amp;gt; Dialect
    end

    subgraph PhysicalLayer[&amp;#34;数据库 DBAPI&amp;#34;]
      DB[(Database&amp;lt;br/&amp;gt;MySQL/PG等)]
    end

    Session --&amp;gt;|发送 SQL 表达式| Engine
    Pool --&amp;gt;|获取连接| DB
    Dialect --&amp;gt;|翻译为原生 SQL| DB
&lt;/pre&gt;

&lt;h3 id=&#34;1-core-层&#34;&gt;1. Core 层
&lt;/h3&gt;&lt;p&gt;Core 层最贴近数据库，它的职责只有一个：&lt;strong&gt;和数据库打交道。&lt;/strong&gt; 即使不用 ORM，你依然可以单独使用 Core 层。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Engine (引擎)&lt;/strong&gt;：整个 SQLAlchemy 的入口点。它本质上是一个配置和资源管理者。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection Pool (连接池)&lt;/strong&gt;：维护与数据库的物理连接，避免频繁建连的巨大开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dialect (方言)&lt;/strong&gt;：负责翻译。由于 MySQL 和 PostgreSQL 的原生 SQL 语法有细微差异，Dialect 负责将通用的指令翻译成特定数据库听得懂的“方言”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;2-orm-层&#34;&gt;2. ORM 层
&lt;/h3&gt;&lt;p&gt;ORM 层建立在 Core 层之上，它的核心设计模式是“&lt;strong&gt;工作单元 (Unit of Work)&lt;/strong&gt;”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session (会话)&lt;/strong&gt;：这是我们在业务代码中最常接触的对象。它不仅是一个数据库连接的代理，更是一个&lt;strong&gt;内存暂存区&lt;/strong&gt;。它追踪了所有查询出来的对象状态（新建、修改、删除），并在适适时机统一提交给 Core 层。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;三-一条查询的生命周期&#34;&gt;三、 一条查询的生命周期
&lt;/h2&gt;&lt;p&gt;理解了双层架构，我们来推导一下执行 &lt;code&gt;session.execute(select(User))&lt;/code&gt; 时，系统到底发生了什么：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代码调用&lt;/strong&gt;：你在业务层触发了查询指令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session 拦截&lt;/strong&gt;：Session 接收到指令，检查内存中是否已经缓存了该数据。如果没有，则向下传递。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core 翻译&lt;/strong&gt;：Engine 接收到抽象的 &lt;code&gt;select()&lt;/code&gt; 表达式，调用对应的 Dialect 将其编译为真正的 SQL 字符串。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取连接&lt;/strong&gt;：Engine 向 Connection Pool 申请一个可用的数据库连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行查询&lt;/strong&gt;：通过底层的 DBAPI (如 &lt;code&gt;pymysql&lt;/code&gt; 或 &lt;code&gt;psycopg2&lt;/code&gt;) 将 SQL 发送给物理数据库执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象映射 (关键)&lt;/strong&gt;：拿到二维表结构的游标结果后，ORM 层将其&lt;strong&gt;反序列化&lt;/strong&gt;为 Python 的 &lt;code&gt;User&lt;/code&gt; 实例，将其挂载到 Session 的追踪名单中，最后返回给你的代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;四-常见问题&#34;&gt;四、 常见问题
&lt;/h2&gt;&lt;h3 id=&#34;1-n1-查询问题&#34;&gt;1. N+1 查询问题
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：获取 100 个用户及其对应的文章列表，原本以为只查了 1 次数据库，看日志却发现数据库跑了 101 条 SQL。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;因果推导&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;因&lt;/strong&gt;：SQLAlchemy 默认对关联关系（如 &lt;code&gt;User.articles&lt;/code&gt;）采用&lt;strong&gt;延迟加载 (Lazy Loading)&lt;/strong&gt;。当你在代码中通过循环访问 &lt;code&gt;user.articles&lt;/code&gt; 时，每次访问都会触发 Session 向数据库发送一条新的查询 SQL以获取该用户的文章。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;果&lt;/strong&gt;：导致数据库 QPS 飙升，接口响应极慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：在查询初始对象时，明确告知 SQLAlchemy 采用&lt;strong&gt;贪婪加载 (Eager Loading)&lt;/strong&gt;。使用 &lt;code&gt;joinedload&lt;/code&gt;（适合一对一）或 &lt;code&gt;selectinload&lt;/code&gt;（适合一对多）在单次查询中把关联数据一起带出：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;selectinload&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;stmt&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;select&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;User&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;selectinload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;User&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;articles&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;session&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;execute&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;stmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;2-内存状态隔离--detachedinstanceerror&#34;&gt;2. 内存状态隔离 —— &lt;code&gt;DetachedInstanceError&lt;/code&gt;
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;现象&lt;/strong&gt;：查出了一个 &lt;code&gt;User&lt;/code&gt; 对象，执行了 &lt;code&gt;session.close()&lt;/code&gt; 后，试图在视图函数里获取 &lt;code&gt;user.name&lt;/code&gt;，程序直接崩溃抛出 &lt;code&gt;DetachedInstanceError&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;因果推导&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;因&lt;/strong&gt;：由架构可知，对象是依赖 &lt;code&gt;Session&lt;/code&gt; 这个上下文环境而存在的。一旦 Session 关闭，物理连接归还给了连接池，这时的 Python 对象就成了失去数据库联系的&lt;strong&gt;游离态 (Detached)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;果&lt;/strong&gt;：此时如果访问某个尚未加载的属性（比如关联属性，或者在 &lt;code&gt;expire_on_commit=True&lt;/code&gt; 设定下被标记为过期的属性），对象试图向背后的 Session 求助去查询数据库，却发现 Session 已经没了，从而抛出异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解法&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;保证所有需要的属性在 Session 生命周期内访问完毕。&lt;/li&gt;
&lt;li&gt;如果确实需要跨层传递对象，可以在创建 Session 时设置 &lt;code&gt;expire_on_commit=False&lt;/code&gt;，但这可能导致读取到过期的脏数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
    </channel>
</rss>
