SQLAlchemy не обновляет / не истекает экземпляры модели с внешними изменениями - PullRequest
2 голосов
/ 27 мая 2019

Недавно я столкнулся со странным поведением 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.

Мой вопрос: почему это вообще происходит?Есть несколько факторов, которые, по-видимому, противоречат текущему поведению:

  1. Я использую параметр expire_on_commit, установленный на True, что, по-видимому, подразумевает, что каждый экземпляр модели, использованный в транзакции, будетпомечается как expired после совершения транзакции.(Цитируя Документация , «Когда True, все экземпляры будут полностью истек после каждого коммита (), так что весь доступ к атрибуту / объекту после завершенной транзакции будет загружен из самого последнего состояния базы данных.»).
  2. Я не обращаюсь к какому-то старому экземпляру модели, а каждый раз выдаю совершенно новый запросНасколько я понимаю, это должно привести к прямому запросу к базе данных, а не к кешированному экземпляру.Я могу подтвердить, что это действительно так, если я включаю журнал отладки sqlalchemy.

Быстрое и грязное решение этой проблемы - вызов db.session.expire_all() сразу после начала транзакции, но это кажетсяочень не элегантный и нелогичный.Я был бы очень рад понять, что не так с тем, как я здесь работаю с sqlalchemy.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...