17

I have a SQLAlchemy model that represents a file and thus contains the path to an actual file. Since deletion of the database row and file should go along (so no orphaned files are left and no rows point to deleted files) I added a delete() method to my model class:

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

This works fine but has one huge disadvantage: The file is deleted immediately before the transaction containing the database deletion is committed.

One option would be committing in the delete() method - but I don't want to do this since I might not be finished with the current transaction. So I'm looking for a way to delay the deletion of the physical file until the transaction deleting the row is actually committed.

SQLAlchemy has an after_delete event but according to the docs this is triggered when the SQL is emitted (i.e. on flush) which is too early. It also has an after_commit event but at this point everything deleted in the transaction has probably been deleted from 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クラスを登録し、セッションが実際にコミットまたはロールバックされたときにキューをクリアまたは評価します。次に、外部ファイルが添付されているクラスについて、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 
于 2012-08-20T14:32:02.417 に答える
1

これは少し難しいようです。SQLトリガーAFTER DELETEがこれに最適なルートであるかどうかを知りたいのですが、それが乾燥しておらず、使用しているSQLデータベースがそれをサポートしているかどうかわからない場合でも、AFAIKsqlalchemyはトランザクションをデータベースにプッシュしますしかし、Imがこのコメントを正しく解釈した場合、それらがいつコミットされたかは実際にはわかりません。

進行中のトランザクションですべての「保留中」のデータを維持するデータベースサーバー自体。変更はディスクに永続的に保持されず、Session.commit()が送信するCOMMITコマンドをデータベースが受信するまで、他のトランザクションに公開されます。

SQLAlchemyからの抜粋:flush()とcommit()の違いは何ですか?sqlalchemyの作成者による..。

于 2012-08-19T02:01:03.650 に答える
1

SQLAlchemyバックエンドがそれをサポートしている場合は、2フェーズコミットを有効にします。次のようなファイルシステムのトランザクションモデルを使用(または書き込み)する必要があります。

  • 権限などをチェックして、ファイルが存在し、最初のコミットフェーズで削除できることを確認します
  • 実際には、2番目のコミットフェーズ中にファイルを削除します。

それはおそらくそれが得ようとしているのと同じくらい良いです。私の知る限り、UnixファイルシステムはXAやその他の2フェーズのトランザクションシステムをネイティブにサポートしていないため、第2フェーズのファイルシステムの削除が予期せず失敗することによるわずかな露出に耐える必要があります。

于 2012-08-19T20:32:18.670 に答える