在实际提交SQLAlchemy对象的删除时执行一些代码

时间:2012-08-19 01:26:15

标签: python postgresql sqlalchemy commit flask-sqlalchemy

我有一个表示文件的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 个答案:

答案 0 :(得分:14)

在带有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()事件完成的。

答案 1 :(得分:5)

这跟随其他基于事件的答案,但我想我会发布这段代码,因为我写它来解决几乎你的确切问题:

代码(下面)注册一个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 

答案 2 :(得分:1)

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

  

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

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

答案 3 :(得分:1)

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

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

这可能和它一样好。据我所知,Unix文件系统本身并不支持XA或其他两阶段事务系统,所以你必须忍受意外发生第二阶段文件系统删除失败的小风险。