最近,我发现 SQLAlchemy 的奇怪行为涉及使用在当前 session 之外所做的更改来刷新/填充模型实例。我创建了以下最小工作示例并能够重现问题。
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 数据库 shell 并开始另一个事务:
so_sqlalchemy=# BEGIN;
BEGIN
so_sqlalchemy=# UPDATE test_models SET field=100 WHERE id=1;
UPDATE 1
so_sqlalchemy=# COMMIT;
COMMIT
当我在 UPDATE
查询后按下 Enter
时,脚本会按预期阻塞,因为我正在发出 SELECT ... FOR UPDATE
在那里查询。但是,当我在数据库 shell 中提交事务时,脚本从以前的值继续(例如,27
)并且没有检测到那个外部事务已将数据库中字段
的值更改为100
。
我的问题是,为什么会发生这种情况?有几个因素似乎与当前行为相矛盾:
- 我正在使用设置为
True
的expire_on_commit
设置,这似乎意味着交易中使用的每个模型实例都将被标记为expired
事务提交后。 (引用 documentation,“当为 True 时,所有实例将在每次 commit() 后完全过期,以便完成事务后的所有属性/对象访问将从最新的数据库状态加载。”)。 - 我不是在访问一些旧的模型实例,而是每次都发出全新的查询。据我了解,这应该导致直接查询数据库而不是访问缓存实例。如果我打开 sqlalchemy 调试日志,我可以确认确实是这种情况。
解决此问题的快速而肮脏的方法是在事务开始后立即调用 db.session.expire_all()
,但这似乎非常不雅且违反直觉。我很乐意了解我在这里使用 sqlalchemy 的方式有什么问题。
最佳答案
我遇到了与 MySQL 非常相似的情况。我需要在代码的数据库操作过程中“查看”来自外部源的表更改。我最终不得不设置 autocommit=True在我的 session 调用中使用 session 的 begin()/commit() 方法来“查看”外部更新的数据。
SQLAlchemy 文档说这是遗留配置:
Warning
“autocommit” mode is a legacy mode of use and should not be considered for new projects.
还要在下一段说:
Modern usage of “autocommit mode” tends to be for framework integrations that wish to control specifically when the “begin” state occurs
所以目前还不清楚到底哪种说法是正确的。
关于python - SQLAlchemy 不会更新/过期具有外部更改的模型实例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56317578/