SQLAlchemy不会通过外部更改来更新/过期模型实例

时间:2019-05-26 21:44:00

标签: python database caching orm sqlalchemy

最近,我遇到了有关SQLAlchemy的奇怪行为,该行为涉及使用当前会话之外进行的更改刷新/填充模型实例。我创建了以下最小的工作示例,并能够重现此问题。


from time import sleep

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

DATABASE_URI = "postgresql://{user}:{password}@{host}:{port}/{name}".format(
    user="postgres",
    password="postgres",
    host="127.0.0.1",
    name="so_sqlalchemy",
    port="5432",
)


class SQLAlchemy:
    def __init__(self, db_url, autocommit=False, autoflush=True):
        self.engine = create_engine(db_url)
        self.session = None

        self.autocommit = autocommit
        self.autoflush = autoflush

    def connect(self):
        session_maker = orm.sessionmaker(
            bind=self.engine,
            autocommit=self.autocommit,
            autoflush=self.autoflush,
            expire_on_commit=True
        )
        self.session = orm.scoped_session(session_maker)

    def disconnect(self):
        self.session.flush()
        self.session.close()
        self.session.remove()
        self.session = None


BaseModel = declarative_base()


class TestModel(BaseModel):
    __tablename__ = "test_models"

    id = Column(BigInteger, primary_key=True, nullable=False)
    field = Column(Integer, nullable=False)


def loop(db):
    while True:
        with db.session.begin():
            t = db.session.query(TestModel).with_for_update().get(1)
            if t is None:
                print("No entry in db, creating...")
                t = TestModel(id=1, field=0)
                db.session.add(t)
                db.session.flush()

            print(f"t.field value is {t.field}")
            t.field += 1
            print(f"t.field value before flush is {t.field}")
            db.session.flush()
            print(f"t.field value after flush is {t.field}")

        print(f"t.field value after transaction is {t.field}")
        print("Sleeping for 2 seconds.")
        sleep(2.0)


def main():
    db = SQLAlchemy(DATABASE_URI, autocommit=True, autoflush=True)
    db.connect()
    try:
        loop(db)
    except KeyboardInterrupt:
        print("Canceled")


if __name__ == '__main__':
    main()

我的requirements.txt文件如下:

alembic==1.0.10
psycopg2-binary==2.8.2
sqlalchemy==1.3.3

如果我运行脚本(我在运行Ubuntu 16.04的笔记本电脑上使用Python 3.7.3),它将按预期每两秒增加一个值:

t.field value is 0
t.field value before flush is 1
t.field value after flush is 1
t.field value after transaction is 1
Sleeping for 2 seconds.
t.field value is 1
t.field value before flush is 2
t.field value after flush is 2
t.field value after transaction is 2
Sleeping for 2 seconds.
...

现在在某个时候我打开postgres数据库外壳并开始另一笔交易:

so_sqlalchemy=# BEGIN;
BEGIN
so_sqlalchemy=# UPDATE test_models SET field=100 WHERE id=1;
UPDATE 1
so_sqlalchemy=# COMMIT;
COMMIT

Enter查询之后按UPDATE时,脚本将按预期方式阻塞,因为我在此处发出SELECT ... FOR UPDATE查询。但是,当我在数据库外壳中提交事务时,脚本从先前的值继续(例如27)并且未检测到外部事务已更改数据库中的field的值为100

我的问题是,为什么会这样呢?有几种因素似乎与当前行为相矛盾:

  1. 我正在使用expire_on_commit设置设为True,这似乎意味着在事务提交后,已在事务中使用的每个模型实例都将标记为expired 。 (引用documentation,“为True时,所有实例将在每次commit()之后完全失效,因此从最近的数据库状态加载完成事务后的所有属性/对象访问。”)。 li>
  2. 我不是在访问某些旧模型实例,而是每次都发出全新的查询。据我了解,这应该导致直接查询数据库而不访问缓存的实例。如果打开sqlalchemy调试日志,我可以确认确实是这种情况。

此问题的快速且肮脏的解决方法是在交易开始后立即致电db.session.expire_all(),但这似乎非常不明智且违反直觉。我很高兴在这里了解我使用sqlalchemy的方式出了什么问题。

1 个答案:

答案 0 :(得分:0)

我遇到了与MySQL非常相似的情况。我需要在代码的数据库操作过程中“查看”对表的更改,这些更改来自外部来源。我最终不得不在会话调用中设置autocommit=True,并使用会话的begin()/ commit()方法来“查看”外部更新的数据。

SQLAlchemy文档说这是旧配置:

  

警告

     

“自动提交”模式是旧的使用模式,不应在新项目中使用。

还要在下一段中说:

  

“自动提交模式”的现代用法往往用于希望专门控制“开始”状态何时发生的框架集成

因此,不清楚哪种说法正确。