导读:本期聚焦于林小满创作的《如何在 SQLAlchemy 中通过虚拟连接创建带延迟加载列的持久化 ORM 对象?》,敬请观看详情。当 ORM 对象需要同时承载主表字段和关联表字段时,把所有 relationship 都改成 eager loading 往往会让查询越来越臃肿。SQLAlchemy 的延迟加载机制允许将这些列设计成虚拟连接,在对象持久化后依然保持按需查询。虚拟连接并不是数据库层的物理 JOIN,而是 relationship 在对象导航时生成的外键查询能力;当访问未加载的关联属性时,Session 会依据主键构造 SELECT,取回需要的列。本文从 relationship 映射、deferred 列和载入策略三个层面说明如何创建带延迟加载列的持久化 ORM 对象,并结合 SQLite 示例演示对象新建、提交、重新加载后访问关联属性的完整过程,同时给出避免 N+1 查询和意外查库的优化方法。

在 SQLAlchemy ORM 中,实体类与数据表通过映射建立联系,而实体之间的关系属性则像一个个虚拟连接,把互相独立的表在对象层面串联起来。当我们创建持久化对象时,如果某些列来自关联表,直接使用 joined eager loading 会让每次查询都付出连接成本,而 lazy loading 则可以在真正访问这些列时才发 SQL。本文围绕如何构造带延迟加载列的持久化 ORM 对象展开,说明虚拟连接的本质、创建方式以及优化细节。

如何在 SQLAlchemy 中通过虚拟连接创建带延迟加载列的持久化 ORM 对象?

虚拟连接与延迟加载的工作机制

ORM 里的虚拟连接并不是数据库表之间真实的 JOIN,而是 relationship 在实体映射中提供的一种对象导航能力。以用户和地址为例,用户表与地址表通过 user_id 外键关联,但在对象层,User 实例上的 addresses 属性并不是数据库里现成的列。当代码第一次读取 user.addresses 时,SQLAlchemy 会根据当前对象的主键值自动生成一条带 WHERE user_id = ? 的 SELECT 语句,把属于该用户的地址行取出来并组织成对象列表。这个过程就是典型的延迟加载。

除 relationship 外,SQLAlchemy 的 deferred 也能让普通列变成延迟加载列。比如文章正文这种大文本列,很少在列表页展示,用 deferred(Column(Text)) 映射后,只有访问 article.content 时才会查询该列。对于一个已经 commit 的持久化对象来说,这种延迟行为仍然有效:Session 会保存对象的主键状态,在需要时重新发起查询。理解这一点是创建轻量持久化 ORM 对象的基础。

from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, deferred

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    addresses = relationship('Address', back_populates='user', lazy='select')

class Address(Base):
    __tablename__ = 'addresses'

    id = Column(Integer, primary_key=True)
    email = Column(String(100))
    user_id = Column(Integer, ForeignKey('users.id'))
    user = relationship('User', back_populates='addresses')

class Article(Base):
    __tablename__ = 'articles'

    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    content = deferred(Column(Text))

上面的映射中,User.addresses 使用 lazy='select',这是 relationship 的默认策略,表示首次访问时才加载关联对象。Article.content 则是一个被 deferred 修饰的普通列,它不会出现在常规 SELECT 中。这样模型层就同时拥有了关系型延迟加载列和主表延迟加载列两种虚拟列。

创建持久化对象并触发延迟加载

所谓持久化 ORM 对象,通常是指与 Session 关联并且对应数据库某一行的对象。新建一个 User 对象并调用 session.add 时,对象处于 pending 状态,commit 后进入 persistent 状态;通过 session.get 或 select 查询出来的对象本身就是 persistent 状态。持久化对象的重要特点是:只要 Session 还处于打开状态,访问未加载的延迟列或关联属性时,ORM 可以重新向数据库发出查询,而不会因为对象已经创建就固定了所有字段。

下面用两个 Session 演示持久化对象上的延迟加载。第一个 Session 创建并提交数据,第二个 Session 重新取出对象。重新取出的 user 只有普通列被加载,关联地址并没有立即查询。

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

session = Session()
user = User(name='alice')
user.addresses.append(Address(email='alice@ipipp.com'))
session.add(user)
session.commit()
user_id = user.id
session.close()

session2 = Session()
loaded_user = session2.get(User, user_id)
print(loaded_user.name)
print(loaded_user.addresses)

从输出的 SQL 顺序可以看到,session2.get(User, user_id) 只查询了 users 表;执行 print(loaded_user.name) 不会产生新的 SQL,而执行 print(loaded_user.addresses) 时会生成一条查询 addresses 表的语句。这说明 addresses 虽然在对象模型中像是 User 的一部分,但它的数据加载被推迟到了真正访问时。这个特性让持久化对象的初始化成本更低,也避免无关查询占用连接资源。

如果要加载的是 deferred 列,行为类似。例如查询 Article 对象后,article.title 不会触发 content 列的查询,只有 article.content 首次访问时才生成 SELECT articles.content FROM articles WHERE id = ?。这里要注意 Session 的生命周期:如果 Session 已经 close,再访问未加载的延迟属性会抛出 DetachedInstanceError,因为这时的对象已经脱离数据库会话,无法再发起查询。

显式覆盖加载策略与 N+1 问题

延迟加载虽然能降低单次查询的字段数量,但在循环访问多个对象的关联属性时容易产生 N+1 查询。例如先查询出 100 个用户,然后在 for 循环里访问 user.addresses,就会额外执行 100 次地址查询。这个问题在持久化对象模型中尤其容易被忽略,因为每次访问看起来只是简单的属性读取,底层却是一次网络往返和 SQL 执行。

SQLAlchemy 提供了 eager loading 来提前将虚拟连接列取出来。比较常用的是 selectinload 和 joinedload。selectinload 会先查主表,再用主键集合执行一条 IN 查询加载所有关联行;joinedload 则直接使用 OUTER JOIN 一次性取回数据。下面示例在查询用户时用 selectinload 同时加载地址,避免循环时的重复查询。

from sqlalchemy import select
from sqlalchemy.orm import selectinload

session3 = Session()
users = session3.scalars(
    select(User).options(selectinload(User.addresses))
).all()
for u in users:
    print(u.name, len(u.addresses))

上述查询会先执行 SELECT users 相关字段,再执行 SELECT addresses WHERE user_id IN (...)。即使循环中访问 u.addresses,也不会产生额外 SQL。selectinload 一般比 joinedload 更适合一对多关系,因为它避免了主表字段被连接结果膨胀的问题。

如果一个关系属性几乎从不需要自动加载,可以把 lazy 参数设置为 raise。这样只要代码在未显式加载的情况下访问该属性,SQLAlchemy 就会抛出 InvalidRequestError,而不是偷偷发 SQL。团队可以借此把所有数据库查询集中在查询语句中声明,提升可维护性。需要时再使用 selectinload 或 joinedload 显式加载,形成一个明确的加载边界。

完整示例:从建模到持久化后的按需取列

下面给出一个可以直接运行的完整示例,把模型定义、数据持久化、对象重载、延迟加载和显式预加载串起来。通过观察 echo 输出的 SQL,可以更直观地理解哪些列被推迟查询。

from sqlalchemy import Column, Integer, String, Text, ForeignKey, create_engine, select
from sqlalchemy.orm import declarative_base, relationship, sessionmaker, deferred, selectinload

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    addresses = relationship('Address', back_populates='user', lazy='select')

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email = Column(String(100))
    user_id = Column(Integer, ForeignKey('users.id'))
    user = relationship('User', back_populates='addresses')

class Article(Base):
    __tablename__ = 'articles'
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    content = deferred(Column(Text))

engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

session = Session()
alice = User(name='Alice')
alice.addresses.append(Address(email='alice@ipipp.com'))
alice.addresses.append(Address(email='bob@ipipp.com'))
article = Article(title='SQLAlchemy Notes', content='Long text...')
session.add_all([alice, article])
session.commit()
alice_id = alice.id
article_id = article.id
session.close()

session = Session()
user = session.get(User, alice_id)
print(user.name)
print([addr.email for addr in user.addresses])

post = session.get(Article, article_id)
print(post.title)
print(post.content)

session2 = Session()
users = session2.scalars(
    select(User).options(selectinload(User.addresses))
).all()
for u in users:
    print(u.name, [a.email for a in u.addresses])
session2.close()

运行后可以清晰地看到:第一次 get(User, alice_id) 只查询 users 表,访问 user.addresses 时查 addresses 表;get(Article, article_id) 只查 id 和 title,访问 post.content 时才查询 content 列。最后 selectinload 则一次性把用户与地址通过两次查询全部拿到。这样的设计让持久化 ORM 对象既保持低初始化开销,又能在需要时按需取列。

在实际项目中,建议根据访问频率决定虚拟连接列的加载策略。列表页和详情页可能使用不同的 Session 查询,列表里只加载概要字段,详情页再延迟加载大文本或关联列表。对于已知需要遍历的集合,务必用 selectinload 或其他 eager loading 提前加载,否则瞬时 QPS 很容易被 N+1 查询拖垮。同时可以把 lazy='raise' 作为团队默认约束,强制开发者显式声明预加载或主动查询,避免无意识的隐藏 SQL。

最终,通过 relationship 和 deferred 构造的延迟加载列,配合 Session 的持久化状态管理,可以让 ORM 对象在代码简洁性和查询效率之间取得平衡。这是 SQLAlchemy 在复杂业务模型中非常实用的高级技巧之一。

SQLAlchemy ORM延迟加载虚拟连接修改时间:2026-10-01 14:09:04

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1001/64267.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。