17

我有一个代表文件的 SQLAlchemy 模型,因此包含实际文件的路径。由于应该删除数据库行和文件(因此没有留下孤立文件,也没有行指向已删除文件)delete(),我在模型类中添加了一个方法:

def delete(self):
    if os.path.exists(self.path):
        os.remove(self.path)
    db.session.delete(self)

这很好用,但有一个巨大的缺点:在提交包含数据库删除的事务之前立即删除文件。

一种选择是在方法中提交delete()- 但我不想这样做,因为我可能没有完成当前事务。所以我正在寻找一种方法来延迟物理文件的删除,直到实际提交删除行的事务。

SQLAlchemy 有一个after_delete事件,但根据文档,这是在发出 SQL 时触发的(即在刷新时),这为时过早。它也有一个after_commit事件,但此时在事务中删除的所有内容都可能已从 SA 中删除。

4

4 回答 4

19

在带有Flask-SQLAlchemy的 Flask 应用程序中使用 SQLAlchemy 时,它会提供一个models_committed信号,该信号接收一个(model, operation)元组列表。使用这个信号做我正在寻找的东西非常容易:

@models_committed.connect_via(app)
def on_models_committed(sender, changes):
    for obj, change in changes:
        if change == 'delete' and hasattr(obj, '__commit_delete__'):
            obj.__commit_delete__()

有了这个通用函数,每个需要 on-delete-commit 代码的模型现在只需要一个方法__commit_delete__(self)并在该方法中执行它需要执行的任何操作。


它也可以在没有 Flask-SQLAlchemy 的情况下完成,但是,在这种情况下,它需要更多代码:

  • 删除需要在执行时记录。这是使用after_delete事件来完成的。
  • 当 COMMIT 成功时,需要处理任何记录的删除。这是使用after_commit事件完成的。
  • 如果事务失败或手动回滚,还需要清除记录的更改。这是使用after_rollback()事件完成的。
于 2012-08-19T13:19:52.930 回答
5

这与其他基于事件的答案一起出现,但我想我会发布这段代码,因为我写它是为了解决你的确切问题:

代码(如下)注册了一个 SessionExtension 类,该类在刷新发生时累积所有新的、更改的和删除的对象,然后在会话实际提交或回滚时清除或评估队列。对于附加了外部文件的类,然后我实现了obj.after_db_new(session)obj.after_db_update(session)和/或obj.after_db_delete(session)SessionExtension 酌情调用的方法;然后,您可以填充这些方法来处理创建/保存/删除外部文件。

注意:我几乎肯定这可以使用 SqlAlchemy 的新事件系统以更简洁的方式重写,并且它还有一些其他缺陷,但它正在生产和工作中,所以我还没有更新它:)

import logging; log = logging.getLogger(__name__)
from sqlalchemy.orm.session import SessionExtension

class TrackerExtension(SessionExtension):

    def __init__(self):
        self.new = set()
        self.deleted = set()
        self.dirty = set()

    def after_flush(self, session, flush_context):
        # NOTE: requires >= SA 0.5
        self.new.update(obj for obj in session.new 
                        if hasattr(obj, "after_db_new"))
        self.deleted.update(obj for obj in session.deleted 
                            if hasattr(obj, "after_db_delete"))
        self.dirty.update(obj for obj in session.dirty 
                          if hasattr(obj, "after_db_update"))

    def after_commit(self, session):
        # NOTE: this is rather hackneyed, in that it hides errors until
        #       the end, just so it can commit as many objects as possible.
        # FIXME: could integrate this w/ twophase to make everything safer in case the methods fail.
        log.debug("after commit: new=%r deleted=%r dirty=%r", 
                  self.new, self.deleted, self.dirty)
        ecount = 0

        if self.new:
            for obj in self.new:
                try:
                    obj.after_db_new(session)
                except:
                    ecount += 1
                    log.critical("error occurred in after_db_new: obj=%r", 
                                 obj, exc_info=True)
            self.new.clear()

        if self.deleted:
            for obj in self.deleted:
                try:
                    obj.after_db_delete(session)
                except:
                    ecount += 1
                    log.critical("error occurred in after_db_delete: obj=%r", 
                                 obj, exc_info=True)
            self.deleted.clear()

        if self.dirty:
            for obj in self.dirty:
                try:
                    obj.after_db_update(session)
                except:
                    ecount += 1
                    log.critical("error occurred in after_db_update: obj=%r", 
                                 obj, exc_info=True)
            self.dirty.clear()

        if ecount:
            raise RuntimeError("%r object error during after_commit() ... "
                               "see traceback for more" % ecount)

    def after_rollback(self, session):
        self.new.clear()
        self.deleted.clear()
        self.dirty.clear()

# then add "extension=TrackerExtension()" to the Session constructor 
于 2012-08-20T14:32:02.417 回答
1

如果您的 SQLAlchemy 后端支持它,请启用两阶段提交。您将需要为文件系统使用(或编写)一个事务模型:

  • 检查权限等以确保文件存在并且可以在第一个提交阶段删除
  • 实际上在第二个提交阶段删除了文件。

这可能和它会得到的一样好。据我所知,Unix 文件系统本身并不支持 XA 或其他两阶段事务系统,因此您将不得不忍受第二阶段文件系统删除意外失败的小风险。

于 2012-08-19T20:32:18.670 回答
1

这似乎有点挑战性,我很好奇 sql 触发器AFTER DELETE是否可能是最好的途径,当然它不会干而且我不确定你使用的 sql 数据库是否支持它,仍然 AFAIK sqlalchemy 将事务推送到数据库但它真的不知道他们什么时候被提交,如果我正确地解释了这个评论:

它是数据库服务器本身,它维护正在进行的事务中的所有“待处理”数据。在数据库收到 Session.commit() 发送的 COMMIT 命令之前,这些更改不会永久保存到磁盘,并公开显示给其他事务。

取自SQLAlchemy:flush() 和 commit() 有什么区别?由 sqlalchemy 的创建者...

于 2012-08-19T02:01:03.650 回答