Python 现代开发学习笔记

(九)从 add、flush 到 commit 与 rollback

1. 跟踪一本书的保存状态

session.add(book) 让会话开始跟踪新对象。这时 book.id 还没有数据库分配的编号。session.flush() 发出 INSERT,编号回到对象中,但事务仍未提交。接下来 commit() 提交,其他会话才能读到这次新增;rollback() 则撤销本次未提交的变化。

# 项目文件:demos/transaction_trace.py
# 分别观察 add、flush、rollback 和 commit 后的记录,新会话只读到已经提交的数据。
from pathlib import Path
from tempfile import TemporaryDirectory

from sqlalchemy import select

from bookapi.config import Settings
from bookapi.database import open_database
from bookapi.models import Book, User

with TemporaryDirectory() as folder:
    engine, factory = open_database(Settings(Path(folder) / "trace.sqlite3"))
    try:
        with factory.begin() as session:
            session.add(User(username="reader", password_hash="本例只观察事务"))
        with factory() as session:
            book = Book(owner_id=1, title="Python Web", finished=False, tags=[])
            session.add(book)
            print("add 后", book.id)
            session.flush()
            print("flush 后", book.id, session.get(Book, book.id) is book)
            session.rollback()
        with factory() as session:
            print("回滚后", list(session.scalars(select(Book.title))))
        with factory.begin() as session:
            session.add(Book(owner_id=1, title="已提交的书", tags=[]))
        with factory() as session:
            print("新会话读取", list(session.scalars(select(Book.title))))
    finally:
        engine.dispose()
# 临时数据库中分别执行 flush 后回滚、正常提交和新会话读取。
uv run python ../demos/transaction_trace.py

输出依次包含 add 后 None、flush 后 1 True、回滚后 [],最后新会话读到 ['已提交的书']。True 来自 session.get(Book, book.id) is book:同一会话已经跟踪这个主键对应的对象,再按主键取得时复用它。这种对象缓存不代表所有 select 查询都跳过数据库。

在同一本待保存书上依次观察 add 后无编号、flush 后取得编号但可回滚,再用另一条提交分支和新会话核对真正保存的结果

2. 统一写入动作的事务收尾

业务函数要在提交成功后返回成功结果。新增与修改也可能发生同名冲突,因此把提交和回滚放进一个上下文管理器:

# 项目文件:api_project/src/bookapi/errors.py
import logging
from contextlib import contextmanager

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session
from starlette.exceptions import HTTPException

logger = logging.getLogger(__name__)


class Problem(Exception):
    def __init__(self, status: int, code: str, message: str):
        self.status, self.code, self.message = status, code, message


@contextmanager
def commit_changes(session: Session):
    try:
        yield
        session.commit()
    except IntegrityError as exc:
        session.rollback()
        # 唯一键冲突转成 409;其他约束错误继续交给数据库异常处理器。
        if getattr(exc.orig, "sqlite_errorname", "") == "SQLITE_CONSTRAINT_UNIQUE":
            raise Problem(409, "duplicate", "相同账号或书名已经存在") from exc
        raise
    except Exception:
        session.rollback()
        raise

with commit_changes(session): 执行到 yield 时进入调用者的代码;调用者完成后执行 commit。发生异常就回滚,再把错误继续抛出。SQLite 报告唯一键冲突时转换为业务异常 Problem(409, ...),后面由统一处理器组织 JSON。

文件开头的 RequestValidationError、JSONResponse 和 HTTPException 留给错误响应处理器:分别表示请求校验错误、直接构造的 JSON 响应和框架 HTTP 异常。SQLAlchemyError 是数据库异常的共同父类。后面写处理器时再沿实际失败请求使用它们。

flush、commit、close 是三件事。显式提交成功才返回新增结果;会话离开依赖时再关闭,归还连接。读请求没有写入要提交,关闭时清理它的事务。数据库异常后的会话先回滚,才能继续正常使用。相关行为见 SQLAlchemy Session 基础。

笔记正文与全部题解 · Ctrl / ⌘ K