提交后如何获取 SQLAlchemy 对象?

Dyl*_*ony 6 python sqlalchemy

问题

我正在使用一种模式在 Redis 中缓存 SQLAlchemy 对象。每当修改和提交实例时,我想清除相关的缓存,以便下次获取时重新加载它。这种清除必须在提交后进行,以避免竞争条件(另一个线程查询缓存、丢失并将过时的数据重新加载到缓存中)。

我已经与这个问题斗争了很长时间,提出了各种有时有效的解决方案,但没有什么万无一失的。这似乎是一个足够简单的问题,应该有一个解决方案。每次向 SQLAlchemy 实例提交更改时,如何触发一些代码?

我尝试过的

活动

我尝试将一些 SQLAlchemy 事件拼接在一起以实现我的目标,并取得了不同程度的成功。监听“after_insert”和“after_update”将告诉我何时修改对象,“after_commit”告诉我修改的内容已保存,因此我有一个方案,其中前两个事件将为“after_commit”注册侦听器,其中依次将对象传递给我的缓存清除函数。像这样:

def _register_after_commit(_: Mapper, __: Connection, target: MyClass) -> None:
    """ Generic callback that adds this function for a target change without params """
    targets.add(target)  # Clear cache uses this set to know which instances to clear
    event.listen(get_session(), "after_commit", clear_cache)

event.listen(MyClass, "after_insert", _register_after_commit)
event.listen(MyClass, "after_update", _register_after_commit)
Run Code Online (Sandbox Code Playgroud)

这在大多数情况下都有效,但DetachedInstanceError在访问目标上的属性时,我偶尔会得到我需要知道的属性以从缓存中清除它们(例如id)。我读到,发生这种情况是因为提交期间自动过期,这导致 SQLAlchemy 想要刷新所有属性。我无法关闭自动过期功能,也无法删除通过此处的每个对象,其中任何一个都可能最终破坏代码库的其他部分。

自定义会话

我创建了自己的会话类,如下所示:

class SessionWithCallback(scoped_session):
    """ A version of orm.Session which can call a method after commit completes """

    def __init__(self, session_factory, scopefunc = None) -> None:
        super().__init__(session_factory=session_factory, scopefunc=scopefunc)
        self._callbacks = {}

    def add_callback(self, func, *args, **kwargs) -> None:
        """
        Adds a callback to be called after commit, ensuring only a single
        instance of the callback for each set of args and kwargs is used
        """
        key = f"{func}.{args}.{kwargs}"
        self._callbacks[key] = (func, args, kwargs)

    def run_callbacks(self) -> None:
        """
        Executes all callbacks
        """
        for (func, args, kwargs) in self._callbacks.values():
            func(*args, **kwargs)
        self._callbacks = {}

    def commit(self) -> None:
        """ Flush and commit the current transaction """
        super().commit()
        self.run_callbacks()

Run Code Online (Sandbox Code Playgroud)

然后,_register_after_commit它将调用当前会话的函数,而不是使用“after_commit”事件add_callback。当仅使用 SQLAlchemy 运行测试时,这似乎有效,但当与使用这些模型并利用 Flask-SQLAlchemy 的 Flask 应用程序集成时,它就崩溃了。我按照说明自定义会话(覆盖create_sessionSQLAlchemy 实例),但是一旦我提交任何内容,我就会收到一个scoped_session没有 attribute 的异常add_callback。我逐步执行,它以某种方式在内部使用我的类,但它给我的会话不是我的类的实例。令人困惑。

我考虑过

  1. 将主键存储在我的侦听器中,然后要求回调打开会话并查询新实例本身(如果需要更多信息)。可能有用,但感觉像是我不需要的额外 I/O。对于一个实例,我可以有多个不同的回调,所有查询都感觉像是一项繁重的工作。
  2. 有一些全局位置来存储回调而不是在会话上,这样我就可以避免该add_callback函数。不过,我仍然需要使这个会话特定且线程安全。在 Flask 中很简单,但 Flask 并不是唯一需要共享此代码的应用程序。
  3. 只需手动清除这些缓存...但这必然会导致开发人员错误。
  4. 生成一些延时作业以从“after_insert/update”中清除缓存。这很快就会变得非常复杂,听起来真的很令人头痛。例如,您如何决定等待多长时间?