Недавно я столкнулся со странным поведением 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
Если я запускаю скрипт (яиспользуйте Python 3.7.3 на моем ноутбуке с Ubuntu 16.04), он будет постепенно увеличивать значение каждые две секунды, как и ожидалось:
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
) и не обнаруживает , что внешняя транзакция изменила значение *От 1022 * в базе данных до 100
.
Мой вопрос: почему это вообще происходит?Есть несколько факторов, которые, по-видимому, противоречат текущему поведению:
- Я использую параметр
expire_on_commit
, установленный на True
, что, по-видимому, подразумевает, что каждый экземпляр модели, использованный в транзакции, будетпомечается как expired
после совершения транзакции.(Цитируя Документация , «Когда True, все экземпляры будут полностью истек после каждого коммита (), так что весь доступ к атрибуту / объекту после завершенной транзакции будет загружен из самого последнего состояния базы данных.»). - Я не обращаюсь к какому-то старому экземпляру модели, а каждый раз выдаю совершенно новый запросНасколько я понимаю, это должно привести к прямому запросу к базе данных, а не к кешированному экземпляру.Я могу подтвердить, что это действительно так, если я включаю журнал отладки sqlalchemy.
Быстрое и грязное решение этой проблемы - вызов db.session.expire_all()
сразу после начала транзакции, но это кажетсяочень не элегантный и нелогичный.Я был бы очень рад понять, что не так с тем, как я здесь работаю с sqlalchemy.