Course Builder个人学习工作台 · 本地模式
Modern Python:面向有经验开发者的回归路线

SQLAlchemy 2.0:事务与持久化

目标:能够用 SQLAlchemy 2.0 建立 SQLite 表、在显式事务中写入和查询消息,并解释 Engine、Session 与 ORM 对象的职责。

SQLAlchemy 提供两层相关 API:Core 负责 SQL 表达式、Engine、Connection 和事务;ORM 在其上增加映射类、Session、identity map 与对象关系。2.0 风格统一使用 select() 等表达式,Core 与 ORM 的事务语义更一致。

关键对象:

  • Engine:数据库连接配置和连接池入口,不等于一条长期打开的连接。
  • Mapped class:Python 类与表结构之间的映射。
  • Session:一个工作单元,跟踪对象并管理事务;不是全局单例。
  • commit/rollback:决定事务边界。没有明确边界,异常时很容易留下半完成操作。

安装与完整实验

uv add sqlalchemy

建立 src/textlab/db.py:

from __future__ import annotations

from sqlalchemy import String, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column


class Base(DeclarativeBase):
    pass


class MessageRow(Base):
    __tablename__ = "messages"

    id: Mapped[int] = mapped_column(primary_key=True)
    text: Mapped[str] = mapped_column(String(5000))
    category: Mapped[str] = mapped_column(String(50))
    priority: Mapped[int] = mapped_column(default=0)


def build_engine(url: str = "sqlite+pysqlite:///:memory:"):
    return create_engine(url, echo=False)


def demo() -> None:
    engine = build_engine()
    Base.metadata.create_all(engine)

    with Session(engine) as session:
        with session.begin():
            session.add(MessageRow(text="我要退款", category="refund", priority=2))

        rows = session.scalars(
            select(MessageRow).where(MessageRow.category == "refund")
        ).all()
        print([(row.id, row.text, row.category) for row in rows])


if __name__ == "__main__":
    demo()

运行:

uv run python -m textlab.db

预期输出:

[(1, '我要退款', 'refund')]

session.begin() 正常退出时 commit,异常退出时 rollback。外层 with Session(...) 负责关闭 Session。select(MessageRow) 返回 ORM entity;session.scalars(...) 直接提取第一列中的映射对象。

内存 SQLite 的生命周期

sqlite:///:memory: 数据通常绑定连接生命周期。测试中如果不同线程/连接需要共享同一个内存库,必须理解 pool 和连接配置;FastAPI TestClient 可能让简单“每次新连接”方案看不到已建表。初学实验可以:

  • 使用临时文件 SQLite;或
  • 测试时显式配置 StaticPool 和 check_same_thread=False;
  • 不要把生产配置机械复制为测试配置。

引导实验:验证 rollback

在 session.begin() 中先 add,再抛 RuntimeError,外层捕获。开启新 Session 查询,预期没有记录。这个实验比只读文档更能建立“事务是 all-or-nothing 边界”的模型。

try:
    with Session(engine) as session:
        with session.begin():
            session.add(MessageRow(text="bad", category="demo"))
            raise RuntimeError("force rollback")
except RuntimeError:
    pass

with Session(engine) as session:
    assert session.scalar(select(MessageRow).where(MessageRow.text == "bad")) is None

独立练习

实现 MessageRepository.add() 和 MessageRepository.get(),让 repository 接收 session factory,而不是内部创建全局 Engine。为 add/get 写临时 SQLite 测试。

参考实现轮廓
from collections.abc import Callable


class MessageRepository:
    def __init__(self, session_factory: Callable[[], Session]) -> None:
        self._session_factory = session_factory

    def add(self, *, text: str, category: str, priority: int) -> int:
        with self._session_factory() as session:
            with session.begin():
                row = MessageRow(text=text, category=category, priority=priority)
                session.add(row)
                session.flush()
                message_id = row.id
            return message_id

    def get(self, message_id: int) -> MessageRow | None:
        with self._session_factory() as session:
            return session.get(MessageRow, message_id)

在大型应用里不一定应返回 detached ORM row;可以转换为 dataclass/Pydantic output。本课先聚焦事务和 repository 边界。

常见失败

  • no such table:检查 create_all 是否对同一 Engine/数据库执行,尤其是内存 SQLite。
  • 写入后查询不到:是否 commit,或是否在另一个独立内存连接?
  • Session 全局复用:Session 不是线程安全的全局容器,应按请求/工作单元创建。

参考资料