
SQLAlchemy 默认会对所有数据库操作(包括 SELECT)隐式启动事务,即使仅执行只读查询;它不提供原生的“无事务只读模式”,但可通过 Session 事件、连接隔离级别或显式 autocommit=True 配置规避不必要的事务开销。
sqlalchemy 默认会对所有数据库操作(包括 select)隐式启动事务,即使仅执行只读查询;它不提供原生的“无事务只读模式”,但可通过 session 事件、连接隔离级别或显式 `autocommit=true` 配置规避不必要的事务开销。
在 SQLAlchemy 中,“事务并非按需创建”,而是按会话生命周期自动管理。官方文档中提到的“virtual transaction”(虚拟事务)并非字面意义的“惰性开启”,而是一种抽象:只要 Session 执行任何 SQL(含 session.execute(select(...))),SQLAlchemy 就会确保当前数据库连接处于一个活跃事务上下文中——哪怕该事务最终只执行了 SELECT 语句且未修改数据。
✅ 默认行为:SELECT 也会触发事务
以下代码看似只读,实则已进入事务:
from sqlalchemy import select from sqlalchemy.orm import Session stmt = select(User).where(User.id == 1) result = session.execute(stmt).scalar_one_or_none() # ← 此时已隐式 BEGIN 事务
底层 PostgreSQL/MySQL/SQLite 会话实际执行了 BEGIN(或等效语句),直到 session.commit() 或 session.rollback()(或 Session 关闭)才结束。这虽不影响正确性,但在高并发只读场景下可能带来锁竞争、连接池占用或 WAL 日志冗余等隐性开销。
✅ 正确的“轻量级只读”方案(推荐)
方案 1:使用 autocommit=True 的独立连接(最轻量)
适用于明确无需 ORM 状态管理的纯查询:
from sqlalchemy import create_engine, text
engine = create_engine("sqlite:///app.db", echo=True)
with engine.connect().execution_options(autocommit=True) as conn:
result = conn.execute(text("SELECT * FROM users WHERE active = 1")).fetchall()
# 无事务上下文,无 Session 开销,立即释放连接
方案 2:设置 isolation_level="READ UNCOMMITTED"(慎用)
降低隔离级别可减少锁,但需评估脏读风险:
engine = create_engine(
"postgresql://...",
isolation_level="READ UNCOMMITTED" # PostgreSQL / MySQL 支持
)
方案 3:自定义只读 Session(兼顾 ORM 与安全)
结合 SessionEvents + info 标记,禁止写入并显式控制事务边界:
from sqlalchemy import event
from sqlalchemy.orm import Session
def make_readonly_session(session: Session):
session.info["readonly"] = True
@event.listens_for(Session, "before_commit")
def prevent_commit(session):
if session.info.get("readonly"):
raise RuntimeError("Read-only session: commit is forbidden")
@event.listens_for(Session, "before_flush")
def prevent_flush(session, flush_context, instances):
if session.info.get("readonly"):
raise RuntimeError("Read-only session: flush is forbidden")
# 使用示例
session = SessionLocal()
make_readonly_session(session)
users = session.query(User).filter(User.status == "active").all() # ✅ 允许
session.add(User(name="hack")) # ❌ 触发 RuntimeError
⚠️ 注意事项
-
不要依赖
session.expire_on_commit=False或expire_all()实现只读:它们仅影响对象状态同步,不改变事务行为; -
Session.begin_nested()或savepoint不解决根本问题:仍运行在父事务内; -
FastAPI 中推荐按端点职责分离 Session:
@app.get("/users", response_model=list[UserSchema]) def get_users(db: Session = Depends(get_readonly_db)): # 专用依赖项 return db.query(User).all()其中
get_readonly_db可返回配置为autocommit=True或绑定只读事件的 Session。
✅ 总结
SQLAlchemy 没有 Spring 的 @Transactional(readOnly = true) 语法糖,但通过组合连接选项、事件钩子与依赖注入,完全可以实现更精细的只读控制。核心原则是:若仅需查询,优先绕过 Session,直连执行;若需 ORM 能力,则用事件防御性拦截写操作,并明确声明意图——这比“等待框架自动优化”更可靠、更可控。










