最近,我遇到了有关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
。
我的问题是,为什么会这样呢?有几种因素似乎与当前行为相矛盾:
expire_on_commit
设置设为True
,这似乎意味着在事务提交后,已在事务中使用的每个模型实例都将标记为expired
。 (引用documentation,“为True时,所有实例将在每次commit()之后完全失效,因此从最近的数据库状态加载完成事务后的所有属性/对象访问。”)。 li>
此问题的快速且肮脏的解决方法是在交易开始后立即致电db.session.expire_all()
,但这似乎非常不明智且违反直觉。我很高兴在这里了解我使用sqlalchemy的方式出了什么问题。
答案 0 :(得分:0)
我遇到了与MySQL非常相似的情况。我需要在代码的数据库操作过程中“查看”对表的更改,这些更改来自外部来源。我最终不得不在会话调用中设置autocommit=True,并使用会话的begin()/ commit()方法来“查看”外部更新的数据。
SQLAlchemy文档说这是旧配置:
警告
“自动提交”模式是旧的使用模式,不应在新项目中使用。
还要在下一段中说:
“自动提交模式”的现代用法往往用于希望专门控制“开始”状态何时发生的框架集成
因此,不清楚哪种说法正确。