为什么这个SQLAlchemy示例提交了对DB的更改?

时间:2010-10-22 15:48:23

标签: python session sqlalchemy commit

这个例子说明了我在构建的应用程序中遇到的一个谜。应用程序需要支持一个允许用户在不实际向DB提交更改的情况下执行代码的选项。但是,当我添加此选项时,我发现即使我没有调用commit()方法,更改仍保留在数据库中。

我的具体问题可以在代码注释中找到。基本目标是更清楚地了解SQLAlchemy何时以及为何将提交给DB。

我更广泛的问题是我的应用程序是应该(a)使用全局Session实例,还是(b)使用全局Session类,从中实例化特定实例。基于这个例子,我开始认为正确的答案是(b)。是对的吗? 修改this SQLAlchemy documentation表示建议使用(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 个答案:

答案 0 :(得分:5)

tl; dr - 更新实际上已提交给数据库 - 它们是正在进行的未提交事务的一部分。


我对您对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()

答案 1 :(得分:1)

由于您声明您实际上在系统上使用MySQL而遇到问题,请检查创建表的引擎类型。默认值为MyISAM,不支持ACID事务。确保您使用InnoDB引擎,该引擎执行ACID事务。

您可以通过

查看表格正在使用哪个引擎
show create table users;

您可以使用alter table更改表的数据库引擎:

alter table users engine="InnoDB";

答案 2 :(得分:1)

<强> 1。示例:只是为了确保(或检查)会话是否未提交更改,只需在会话对象上调用expunge_all即可。这很可能证明这些变化实际提交:

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

<强> 2。 mysql:正如您已经提到的,sqlite示例可能无法反映您在使用mysql时实际看到的内容。正如sqlalchemy - MySQL - Storage Engines中所述,问题的最可能原因是使用非事务性存储引擎(如MyISAM),这导致autocommit执行模式。

第3。会话范围:虽然有一个全局会话听起来像寻找问题,但是对每个微小的请求使用新会话也不是一个好主意。您应该将会话视为事务/ unit-of-work。我发现contextual sessions的用法是两个世界中最好的,你不必在方法调用的层次结构中传递会话对象,同时你在多个方面给予了相当好的安全性。线程环境。我偶尔会使用本地会话,我知道我不想与当前正在运行的事务(会话)进行交互。

答案 3 :(得分:0)

请注意,create_session()的默认值与sessionmaker()的默认值相反:autoflush和expire_on_commit为False,autocommit为True。

答案 4 :(得分:0)

当你调用modify_ages()并且你已经提交到数据库时,已经实例化了

global_session。如果在提交后重新实例化global_session,它应该启动一个新事务。

我猜是因为你已经提交并重新使用同一个对象,所以每个额外的修改都会自动提交。