Claude Opus 4.6 vs ChatGPT 5.3:一次 SQLAlchemy 事务问题的复盘


摘要

记录一次vibe coding中的典型失误:Claude Opus 4.6 未意识到 Python enum 类在 SQLAlchemy 中的查询陷阱, 甚至编造了连接池快照理论,并且胆敢修改数据库隔离级别。ChatGPT 5.3 则准确指出了核心问题。 这一案例揭示了AI为了逻辑自洽会编造理论,缺乏对变更后果(胆敢修改项目默认数据库隔离级别)。


一、真实问题:Python Enum 的查询陷阱

在优化 DAO 层状态枚举时,Claude 引入 Python enum 类:

import enum

class Status(enum.Enum):
    PENDING = 1
    ACTIVE = 2

# 错误:比较的是枚举对象,而非其值,结果是查不出值的
session.query(User).filter(User.status == Status.ACTIVE).all()

# 正确:使用 .value 比较
session.query(User).filter(User.status == Status.ACTIVE.value).all()

SQLAlchemy框架传入枚举对象筛选时默认比较枚举对象本身,而非其底层值,导致查询无结果。 这是代码层面的类型匹配问题,与事务隔离级别无关。


二、Claude 的错误分析路径

2.1 错误归因:事务隔离级别

Claude 将问题归因于 MySQL 的”可重复读”隔离级别,认为符合”A写入数据,B查询不到”的现象。

2.2 编造的理论:连接池快照复用

为自圆其说,Claude 构建了一套错误理论:

  • SQLAlchemy 连接池复用旧连接
  • 旧连接保留之前的 ReadView(MVCC 快照)
  • 只读事务的 commit() 是空操作,不会销毁快照

2.3 危险的修复建议

基于上述错误理论,Claude 建议:

  1. 修改数据库隔离级别:将”可重复读”改为”读已提交”
  2. 实现强制刷新机制:在查询前直接对底层连接执行 COMMIT

为了不影响项目中其它引用,我命令Claude只修改新增部分,它产出了如下的代码建议。每次事务先commit一下以获取最新的数据库快照。

@contextmanager
def _get_fresh_session(db_name: str = 'chatbi'):
    """WebSocket 场景下获取 session,查询前强制提交以刷新 MVCC 快照。

    连接池复用时,旧连接的 ReadView 不会自动更新(ROLLBACK 不刷新快照),
    通过底层连接直接执行 COMMIT(绕过 SQLAlchemy 事务管理),
    让 MySQL 在下一条 SQL 时重建 ReadView,从而读取到其他事务最新提交的数据。

    注意:不能用 session.execute(text("COMMIT")),SQLAlchemy 2.x 的 autobegin
    机制会将其包在当前事务内执行,MySQL 不会真正结束事务,ReadView 不会刷新。
    """
    with db_manager.get_session(db_name) as session:
        # 用底层连接直接发 COMMIT,绕过 SQLAlchemy 事务层,真正刷新 ReadView
        conn = session.connection()
        conn.execute(text("COMMIT"))
        yield session

当质疑”HTTP 请求使用相同连接池为何无此问题”时,Claude 狡辩称 HTTP 请求有写操作所以 commit() 是真实提交。 这一解释十分离谱,如果是这样,那http请求这么多只读操作,岂不是都会乱套?为此我在codeBuddy中切换ChatGPT5.3,查看历史会话消息 请它来独立评估一下。


三、ChatGPT 5.3 的正确评估

3.1 核心错误识别

ChatGPT 5.3 指出 Claude 的根本错误:将”事务级快照”误解为”连接级快照”

3.2 关键论点

(1)ReadView 是事务级的,非连接级

在 MySQL InnoDB 中:

  • ReadView 与事务绑定,非 TCP 连接永久绑定
  • 事务正常结束(COMMITROLLBACK)后,ReadView 即销毁
  • 下一次事务会建立新的 ReadView

结论:连接池复用 ≠ 快照复用。

(2)当前 Session 是短事务模式

@contextmanager
def get_session(self, db_name: str = 'adw'):
    session = self._session_factories[db_name]()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

每轮 DAO 调用新建 Session,使用结束后 commit() + close(),事务生命周期是短的。

(3)只读事务的 COMMIT 非空操作

  • COMMIT 包含两层语义:结束事务(控制层)+ 刷脏页(持久化层)
  • 只读事务仅影响第 2 层,第 1 层(结束事务、销毁 ReadView)依然有效
  • “无数据落盘”≠“事务未结束”

(4)底层 COMMIT 破坏 ORM 事务边界

手动对底层连接发 COMMIT 会让连接真实状态与 SQLAlchemy Session 维护的状态不一致,属于不规范写法。

(5)绕过 session.commit() 不合理

问题不在于”数据库不认”,而在于破坏 SQLAlchemy 对事务边界的一致性维护。


四、Claude 的自我反思

下面放一下claude认错原文截图,殊不知这么“专业”、“详实”的注释正式出自它自己笔下。😂 img.png

4.1 权威偏见

看到注释写得专业详实(“MVCC 快照”、“ReadView”、“autobegin”),便直接采信,未独立验证核心前提。

4.2 确认偏见

面对”HTTP 请求为何无此问题”的质疑,思维模式是”在已有结论框架内解释新问题”,而非”用新问题检验已有结论”。

4.3 模式切换延迟

直到用户直接问”这个说法是否正确”,才切换到”独立评估”模式。教训:关键判断应主动切换至此模式。

4.4 归因问题

将旧观点归因于”另一个 AI”模糊了自己犯错的事实,更诚实的做法应是直接承认错误。


五、结论

_get_fresh_session() 缺乏理论依据,应移除。

论点实际情况
ReadView 复用❌ 事务级,非连接级
只读 commit 无效❌ 结束事务语义依然有效
底层 COMMIT 更安全❌ 破坏 ORM 事务边界

根本原因:Python enum 查询应使用 .value,而非枚举对象本身。