【问题标题】:Execute some code when an SQLAlchemy object's deletion is actually committed在实际提交 SQLAlchemy 对象的删除时执行一些代码
【发布时间】:2012-08-14 22:46:30
【问题描述】:

我有一个代表文件的 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 中删除。

【问题讨论】:

    标签: python postgresql sqlalchemy commit flask-sqlalchemy


    【解决方案1】:

    当在带有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 event 完成。
    • 当 COMMIT 成功时,需要处理任何记录的删除。这是使用after_commit event 完成的。
    • 如果事务失败或手动回滚,还需要清除记录的更改。这是使用after_rollback() 事件完成的。

    【讨论】:

    • 这在应用程序中的 every 模型的 every 提交时调用。这不是矫枉过正吗?
    • 为什么?删除不是最常见的操作,一个函数调用不会增加太多开销。
    • 但是models_committed不仅限于delete,而是调用了最常见的inserts和updates。来自doc,“当更改模型提交到数据库时发送此信号”
    • 没错,没想到。无论如何,仍然只是很小的开销。
    • 最近flask-sqlalchemy添加了SQLALCHEMY_TRACK_MODIFICATIONS来启用/禁用信号子系统。能够在每个模型的基础上启用它会很漂亮,但在决定是否发出信号时可能会重新触发性能问题:D
    【解决方案2】:

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

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

    注意:我几乎肯定这可以使用 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 
    

    【讨论】:

    • 事实上 SessionExtension 现在已被弃用,但正如您所指出的,这可以用几乎完全相同的代码在新的事件框架中重写——您不会从 SessionExtension 继承,而是添加一个方法来将所有方法注册为给定会话或 sessionfactory 上的事件侦听器(或者您可以使用 event.listen_for 装饰器,但这具有将实现绑定到特定 Session 类的缺点(授予一个人使用应该没问题)。
    【解决方案3】:

    如果您的 SQLAlchemy 后端支持它,请启用 two-phase commit。您需要为文件系统使用(或编写)一个事务模型:

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

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

    【讨论】:

    • 听起来很复杂 - 但对于更复杂的系统来说可能是个好主意。
    【解决方案4】:

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

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

    取自 SQLAlchemy: What's the difference between flush() and commit()? 由 sqlalchemy 的创建者...

    【讨论】:

    • 触发器在数据库中运行 - 所以它们并不是一个很好的选择。实际上它们根本不是一个选项,因为 postgresql 用户根本无法访问应该删除的文件。
    • 这就是为什么我问你的后端数据库是什么,有一些允许系统调用的 dbms,我认为 MSSQL 就是其中之一,postgres 也有 plperlu postgresql.org/docs/8.0/static/plperl-trusted.html 可以运行 perl 命令在实际的 sql 中,它反过来可能能够进行系统调用,请记住这有点危险,postgres 建议 plperl 受到更多限制,但是你想删除文件然后 plperlu 可能是一种可能的路线...
    猜你喜欢
    • 2013-09-02
    • 1970-01-01
    • 2019-07-15
    • 1970-01-01
    • 1970-01-01
    • 2016-12-19
    • 1970-01-01
    • 2017-05-20
    • 1970-01-01
    相关资源
    最近更新 更多