为什么这个SQLAlchemy示例会提交更改到数据库?

4 投票
5 回答
3284 浏览
提问于 2025-04-16 05:55

这个例子展示了我在构建一个应用时遇到的一个小困惑。我的应用需要有一个选项,让用户可以运行代码,但不把更改保存到数据库里。然而,当我添加了这个选项后,我发现即使我没有调用 commit() 方法,修改还是被保存到了数据库。

我具体的问题可以在代码注释里找到。我的主要目标是更清楚地理解 SQLAlchemy 什么时候以及为什么会把数据提交到数据库。

我还有一个更广泛的问题,就是我的应用应该使用 (a) 一个全局的 Session 实例,还是 (b) 使用一个全局的 Session 类,从中创建特定的实例。根据这个例子,我开始觉得正确的答案是 (b)。这样理解对吗? 编辑这个 SQLAlchemy 的文档 也建议使用 (b)。

import sys

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id   = Column(Integer, primary_key = True)
    name = Column(String)
    age  = Column(Integer)

    def __init__(self, name, age = 0):
        self.name = name
        self.age  = 0

    def __repr__(self):
        return "<User(name='{0}', age={1})>".format(self.name, self.age)

engine = create_engine('sqlite://', echo = False)
Base.metadata.create_all(engine)

Session = sessionmaker()
Session.configure(bind=engine)

global_session = Session() # A global Session instance.
commit_ages    = False     # Whether to commit in modify_ages().
use_global     = True      # If True, modify_ages() will commit, regardless
                           # of the value of commit_ages. Why?

def get_session():
    return global_session if use_global else Session()

def add_users(names):
    s = get_session()
    s.add_all(User(nm) for nm in names)
    s.commit()

def list_users():
    s = get_session()
    for u in s.query(User): print ' ', u

def modify_ages():
    s = get_session()
    n = 0
    for u in s.query(User):
        n += 10
        u.age = n
    if commit_ages: s.commit()

add_users(('A', 'B', 'C'))
print '\nBefore:'
list_users()
modify_ages()
print '\nAfter:'
list_users()

5 个回答

1

因为你提到你在使用MySQL的系统上遇到了问题,所以先检查一下这个表是用什么引擎创建的。默认情况下是MyISAM,这种引擎不支持ACID事务。你需要确保使用的是InnoDB引擎,它是支持ACID事务的。

你可以通过以下命令查看一个表使用的引擎:

show create table users;

如果你想更改一个表的数据库引擎,可以使用alter table命令:

alter table users engine="InnoDB";
1

1. 示例:为了确保会话没有提交任何更改,只需要在会话对象上调用 expunge_all。这通常可以证明这些更改并没有被实际提交:

....
print '\nAfter:'
get_session().expunge_all()
list_users()

2. mysql:正如你提到的,sqlite 的例子可能和你在使用 mysql 时看到的情况不太一样。根据 sqlalchemy - MySQL - 存储引擎 的文档,你遇到问题的最可能原因是使用了非事务性的存储引擎(比如 MyISAM),这会导致执行时处于 autocommit 模式。

3. 会话范围:虽然有一个全局会话听起来像是在寻找问题,但每次小请求都使用新的会话也不是个好主意。你应该把会话看作是一个事务/工作单元。我觉得使用 上下文会话 是两全其美的选择,这样你就不需要在方法调用的层级中传递会话对象,同时在多线程环境中也能获得很好的安全性。我偶尔会使用 局部 会话,当我知道不想与当前正在运行的事务(会话)交互时。

5

总结一下 - 更新实际上并没有被提交到数据库中,它们只是一个正在进行的未提交事务的一部分。


我对你调用create_engine()的代码做了两个小改动。(除了这一行,我的代码和你发的完全一样。)

第一个改动是

engine = create_engine('sqlite://', echo = True)

这个改动提供了一些有用的信息。我不会在这里贴出全部输出,但请注意,直到第二次调用list_users()时,才会发出SQL更新命令:

...
After:
xxxx-xx-xx xx:xx:xx,xxx INFO sqlalchemy.engine.base.Engine.0x...d3d0 UPDATE users SET age=? WHERE users.id = ?
xxxx-xx-xx xx:xx:xx,xxx INFO sqlalchemy.engine.base.Engine.0x...d3d0 (10, 1)
...

这说明数据并没有被保存,而是保留在会话对象中。

我做的第二个改动是将数据库保存到一个文件中,使用了

engine = create_engine('sqlite:///db.sqlite', echo = True)

再次运行脚本,第二次调用list_users()的输出和之前一样:

<User(name='A', age=10)>
<User(name='B', age=20)>
<User(name='C', age=30)>

但是,如果你现在打开我们刚创建的数据库并查询它的内容,你会发现新增的用户被保存到了数据库中,但年龄的修改没有被保存:

$ sqlite3 db.sqlite "select * from users"
1|A|0
2|B|0
3|C|0

所以,第二次调用list_users()获取的值来自会话对象,而不是数据库,因为有一个未提交的事务正在进行中。为了证明这一点,可以在你的脚本末尾添加以下几行:

s = get_session()
s.rollback()
print '\nAfter rollback:'
list_users()

撰写回答