【问题标题】:Why does this SQLAlchemy example commit changes to the DB?为什么这个 SQLAlchemy 示例将更改提交到数据库?
【发布时间】:2011-04-29 06:03:12
【问题描述】:

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

我的具体问题可以在代码 cmets 中找到。基本目标是更清楚地了解 SQLAlchemy 何时以及为什么会提交到数据库。

我更广泛的问题是我的应用程序是否应该 (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()

【问题讨论】:

  • 我试图回答您的具体问题,但是,是的,参考您的 Edit:,我肯定会走 (b) 路线。一个全局会话实例只是询问与此完全相同的问题。

标签: python session sqlalchemy commit


【解决方案1】:

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


我对您对 create_engine() 的调用进行了 2 个单独的更改。 (除了这一行,我使用的代码与发布的完全相同。)

第一个是

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()

【讨论】:

  • 非常感谢。这看起来是正确的答案。不幸的是,这意味着我的示例脚本没有展示在我的完整应用程序中观察到的问题——即使我避免调用commit(),它实际上也正在向 MySQL DB 提交更改。是时候进行更多调查了!
  • 我想说的是,在调试时仅使用 echo=True 进行 create_engine 调用将是缩小发生情况的最佳工具之一。
【解决方案2】:

既然您声明您实际在您看到问题的系统上使用 MySQL,请检查创建表时使用的引擎类型。默认是 MyISAM,它不支持 ACID 事务。确保您使用的是 InnoDB 引擎,它确实执行 ACID 事务。

你可以看到一个表正在使用哪个引擎

show create table users;

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

alter table users engine="InnoDB";

【讨论】:

    【解决方案3】:

    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 的用法是两全其美,您不必在方法调用的层次结构中传递会话对象,同时在多线程环境中为您提供了很好的安全性.我确实偶尔使用本地会话,我知道我不想与当前正在运行的事务(会话)进行交互。

    【讨论】:

    • 谢谢。这很有帮助。
    【解决方案4】:

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

    【讨论】:

    • 感谢您的回复,但我不知道这是如何解决问题的。
    • 尝试 sessionmaker() 而不是 create_session()。
    • 据我所知,我已经在使用sessionmaker(),而不是create_session()。我错过了什么吗?
    【解决方案5】:

    当你调用 modify_ages() 并且你已经提交到数据库时,global_session 已经被实例化了。如果您在提交后重新实例化 global_session,它应该会启动一个新事务。

    我的猜测是,由于您已经提交并且正在重复使用同一个对象,因此每个额外的修改都会自动提交。

    【讨论】:

      猜你喜欢
      • 2013-12-20
      • 1970-01-01
      • 2012-01-08
      • 1970-01-01
      • 2021-12-26
      • 1970-01-01
      • 2012-02-21
      • 1970-01-01
      • 2011-08-26
      相关资源
      最近更新 更多