【问题标题】:Querying objects added to a non committed session in SQLAlchemy查询添加到 SQLAlchemy 中未提交会话的对象
【发布时间】:2018-01-13 13:41:37
【问题描述】:

所以我在没有太多上下文的情况下提出了这个问题,并且被否决了,让我们再试一次......

首先,我不遵循 SQLAlchemy 的 session.add 背后的逻辑。我知道它将对象排队以进行插入,并且我知道session.query 在连接的数据库中而不是在会话中查找,但是在 SQLAlchemy 中是否有可能在不先执行session.flush 的情况下查询会话?我对 session.query 的期望是它查询会话...

我现在在None 来自session.query().first() 之后手动查找session.new


我不想在session.flush 之前做session.flush 有两个原因,

  • 基于效率的担忧(如果我仍在用户可能想要回滚的会话中,我为什么要写入数据库并查询数据库?);
  • 二是因为我采用了fairly large program,它设法定义了自己的Session,其实例导致flush也提交。

所以这个问题的真正核心是,谁在帮我在 github 上的 GPL 程序中找到错误!

这是一个代码 sn-p,在 bauble/ghini 中有一个令人惊讶的行为:

# setting up things in ghini
# <replace-later>
import bauble
import bauble.db as db
db.open('sqlite:///:memory:', verify=False)
from bauble.prefs import prefs
import bauble.pluginmgr as pluginmgr
prefs.init()
prefs.testing = True
pluginmgr.load()
db.create(True)
Session = bauble.db.Session
from bauble.plugins.garden import Location
# </replace-later>

# now just plain straightforward usage
session = Session()

session.query(Location).delete()
session.commit()
u0 = session.query(Location).filter_by(code=u'mario').first()
print u0

u1 = Location(code=u'mario')
session.add(u1)
session.flush()

u2 = session.query(Location).filter_by(code=u'mario').one()
print u1, u2, u1==u2

session.rollback()
u3 = session.query(Location).filter_by(code=u'mario').first()
print u3

这里的输出是:

None
mario mario True
mario

这里有我认为只是设置数据库的标准简单代码:

from sqlalchemy import Column, Unicode

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

class Location(Base):
    __tablename__ = 'location'
    code = Column(Unicode(64), index=True, primary_key=True)
    def __init__(self, code=None):
        self.code = code
    def __repr__(self):
        return self.code

from sqlalchemy import create_engine
engine = create_engine('sqlite:///joindemo.db')

Base.metadata.create_all(engine)

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine, autoflush=False)

有了这个,上面同样的代码 sn-p 的输出就不那么令人惊讶了:

None
mario mario True
None

【问题讨论】:

  • 您已禁用 autoflush,因此您的 mario, none, False 可以正常工作。 针对数据库触发查询,如果您不手动或自动将会话中保存的更改刷新到数据库,则查询无法看到这些更改。而且flush并不意味着提交,所以还有其他事情发生了,你应该提供一个minimal reproducible example关于这个,或者通过“回滚不起作用”来更清楚你的意思。
  • 我禁用了 autoflush,因为正如您也提到的,我试图生成一个 Minimal 示例,从 a program I adopted 开始,其中单元测试依赖于禁用 autoflush。
  • »flush 并不意味着提交,所以还有其他事情发生«,我试图在 ghini.desktop 代码中查找它,但我不认识它。并且该代码远非 Minimal。
  • »对 DB 触发查询« — 因为 query 是会话的一种方法,我假设我正在查询会话,所以令我惊讶的是 session.query 不返回对象在session.new.
  • 如果有人想帮我看看deeper in the code?谢谢。

标签: python transactions sqlalchemy


【解决方案1】:

小玩意中的刷新最终发出 COMMIT 的原因是他们处理历史表的line 133 in db.py

table.insert(dict(table_name=mapper.local_table.name,
                  table_id=instance.id, values=str(row),
                  operation=operation, user=user,
                  timestamp=datetime.datetime.today())).execute()

而不是使用传入的事务连接在事件处理程序中发出额外的 SQL,他们应该,他们按原样执行语句本身,这意味着它最终使用引擎作为绑定(通过表的元数据找到)。使用引擎执行具有autocommit 行为。由于bauble always usesSingletonThreadPool,每个线程只有一个连接,因此该语句最终也会提交刷新的更改。我想知道这个错误是否是 bauble 禁用 autoflush...的原因...

解决方法是更改​​事件处理以使用事务连接:

class HistoryExtension(orm.MapperExtension):
    """
    HistoryExtension is a
    :class:`~sqlalchemy.orm.interfaces.MapperExtension` that is added
    to all clases that inherit from bauble.db.Base so that all
    inserts, updates, and deletes made to the mapped objects are
    recorded in the `history` table.
    """
    def _add(self, operation, mapper, connection, instance):
        """
        Add a new entry to the history table.
        """
        ...  # a ton of code here
        table = History.__table__
        stmt = table.insert(dict(table_name=mapper.local_table.name,
                                 table_id=instance.id, values=str(row),
                                 operation=operation, user=user,
                                 timestamp=datetime.datetime.today()))
        connection.execute(stmt)

    def after_update(self, mapper, connection, instance):
        self._add('update', mapper, connection, instance)

    def after_insert(self, mapper, connection, instance):
        self._add('insert', mapper, connection, instance)

    def after_delete(self, mapper, connection, instance):
        self._add('delete', mapper, connection, instance)

值得注意的是,MapperExtension 自 0.7 版以来已被弃用。


关于你对我引用"Session Basics" 的会议的看法,你真的应该仔细阅读:

在最一般的意义上,Session 建立与数据库的所有对话,并代表您在其生命周期内加载或与之关联的所有对象的“保留区”。它提供了获取Query对象的入口点,该对象使用Session对象的当前数据库连接向数据库发送查询,...

"Is the Session a cache?":

是的……不。它在某种程度上用作缓存,因为它实现了身份映射模式,并存储键控到其主键的对象。但是,它不执行任何类型的查询缓存。这意味着,如果你说session.query(Foo).filter_by(name='bar'),即使Foo(name='bar') 就在那里,在身份映射中,会话对此一无所知。它必须向数据库发出 SQL,取回行,然后当它看到行中的主键时,它可以查看本地标识映射并看到对象已经存在。只有当您说query.get({some primary key}) 时,Session 才不必发出查询。

所以:

我对读取 session.query 的东西的期望是它查询会话......

你的期望是错误的。 Session 处理与 DB 的对话——在 other things 中。

我不想在 session.query 之前执行 session.flush 有两个原因,

  • 基于效率的担忧(如果我仍在用户可能想要回滚的会话中,我为什么要写入数据库并查询数据库?);

因为您的数据库可能会进行验证、拥有触发器并为某些列(主键、时间戳等)生成值。您认为您要插入的数据最终可能会在数据库中出现其他内容,Session 绝对无法知道这一点。

另外,SQLAlchemy 为什么要自己实现一种内存数据库,有自己的查询引擎,以及同步 2 个数据库带来的所有问题? SQLAlchemy 如何支持您查询的不同数据库的所有不同操作和功能?您的简单等式谓词示例只是触及表面。

当您回滚时,您将回滚数据库的事务(连同会话中未刷新的更改)。

  • 二是因为我采用了一个相当大的程序,它设法定义了自己的 Session,其实例导致刷新也提交。

由事件处理错误引起。

【讨论】:

  • 哇...我正忙于其他事情,这不是我能在 2 分钟内阅读的内容,所以我会推迟。在此之前非常感谢您。
  • (我正在添加对来源的贡献。我正在将您的名字添加到版权所有者中。如果您愿意,我可以在版权行中添加您的电子邮件地址)。
猜你喜欢
  • 1970-01-01
  • 2012-09-09
  • 1970-01-01
  • 1970-01-01
  • 2014-08-09
  • 2023-03-29
  • 1970-01-01
  • 1970-01-01
  • 2012-01-05
相关资源
最近更新 更多