SQLAlchemy:如何避免 ORM 缓存和数据库之间的这种不一致

SQLAlchemy: How do I avoid this inconsistency between ORM cache and DB

这是我的问题的简化版本。我有一个程序 query.py:

import time
from models import Ball, session

time.sleep(1)
r = session.query(Ball).filter(Ball.color=='red').first()
print(f'Red ball color is {r.color}')
time.sleep(2)
b = session.query(Ball).filter(Ball.color=='blue').first()
print(f'Blue ball color is {b.color}')
print(f'Red ball id is {r.id}, blue ball id is {b.id}')

当我 运行 query.py 同时 modify.py (包含在下面)时,我得到以下输出:

$ python modify.py &! python query.py
Red ball color is red                                                                                                                                                   
Blue ball color is red                                                                                                                                                  
Red ball id is 1, blue ball id is 1                                                                                                                  

问题是蓝球是红的!

这里是models.py的内容:

import sqlalchemy as sa
import sqlalchemy.orm as sao
import sqlalchemy.ext.declarative as saed

Base = saed.declarative_base()

class Ball(Base):
    __tablename__ = 'ball'
    id = sa.Column(sa.Integer, primary_key=True)
    color = sa.Column(sa.String)

engine = sa.create_engine('sqlite:///test.db')
Base.metadata.create_all(engine)
session = sao.Session(engine)

这里是 modify.py:

import time
from models import Ball, session

session.query(Ball).delete()
b = Ball(color='red')
session.add(b)
session.commit()
time.sleep(2)
b.color = 'blue'
session.add(b)
session.commit()

我发现我的数据库查询(看到最新的数据库状态)和通过 SQLAlchemy identiy 映射为我的数据库查询返回的对象(陈旧,反映了数据库状态)之间存在不一致,这很奇怪第一次读取有问题的行)。我知道在每个查询之前在 query.py 进程中重新启动我的事务将使身份映射中的缓存对象无效并导致此处的蓝色球为蓝色,但这是不可能的。

如果蓝球是蓝色的——即如果数据库查询和它返回的对象一致——或者如果蓝球查询返回 None——即如果并发数据库修改,我会很高兴在查询事务中不可见。但是我好像卡在了中间。

似乎潜在的问题是 SQLite 支持在 Python 中默认是有问题的,而 SQLAlchemy 有意地继承了这个有问题的行为。 我最终想出了如何获得两种可能的正确行为,即通过在不取消当前交易的情况下使身份无效 map/cache 使蓝球变成蓝色,或者通过 [=59] 查询蓝球 return None =]将 query.py 放入正确隔离的事务中。

实现隔离/进行蓝球查询returnNone

我发现通过将 isolation_level=<level> 传递给 create_engine 来设置隔离级别没有任何效果,即这没有提供使 query.py 运行 在与 modify.py 的写入隔离的事务中,其中对蓝球的查询将 return None。在读到 isolation levels in SQLite it seems that setting the isolation level to SERIALIZABLE should accomplish this, but it does not. However, the SQLAlchemy documentation warns 默认情况下 SQLite 事务被破坏后:

In the section Database Locking Behavior / Concurrency, we refer to the pysqlite driver’s assortment of issues that prevent several features of SQLite from working correctly. The pysqlite DBAPI driver has several long-standing bugs which impact the correctness of its transactional behavior. In its default mode of operation, SQLite features such as SERIALIZABLE isolation, transactional DDL, and SAVEPOINT support are non-functional, and in order to use these features, workarounds must be taken.

该页面继续建议解决方法以获得正常运行的事务,这些解决方法对我有用。即,将以下内容添加到 models.py 的底部实现了蓝色球查询 returns None:

的隔离行为
@sa.event.listens_for(engine, "connect")
def do_connect(dbapi_connection, connection_record):
    # disable pysqlite's emitting of the BEGIN statement entirely.
    # also stops it from emitting COMMIT before any DDL.
    dbapi_connection.isolation_level = None

@sa.event.listens_for(engine, "begin")
def do_begin(conn):
    # emit our own BEGIN
    conn.exec_driver_sql("BEGIN")

进行此更改后,输出变为

$ python modify.py &! python query.py
Red ball color is red                                                                                                                                                   
Traceback (most recent call last):                                                                                                                                      
  File "query.py", line 10, in <module>
    print(f'Blue ball color is {b.color}')
AttributeError: 'NoneType' object has no attribute 'color'

modify.py 中使球变蓝的 DB 写入在 query.py.

中的(隐式)事务中不可见

使查询和 returned 对象一致/使蓝球变成蓝色

另一方面,要获得蓝色球为蓝色的行为,在每次查询之前使用 session.expire_all() 使 cache/identity 映射无效就足够了。即,将 query.py 更改为以下作品:

import time
from models import Ball, session

time.sleep(1)
r = session.query(Ball).filter(Ball.color=='red').first()
print(f'Red ball color is {r.color}')
time.sleep(2)
# Added this line:
session.expire_all()
b = session.query(Ball).filter(Ball.color=='blue').first()
print(f'Blue ball color is {b.color}')
print(f'Red ball id is {r.id}, blue ball id is {b.id}')

进行此更改后,输出变为

$ python modify.py &! python query.py                                                                                                                                
Red ball color is red                                                                                                                                                   
Blue ball color is blue                                                                                                                                                 
Red ball id is 1, blue ball id is 1