我有一个表示文件的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中删除。
答案 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。after_commit
event。答案 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或其他两阶段事务系统,所以你必须忍受意外发生第二阶段文件系统删除失败的小风险。