Выполните некоторый код, когда действительно удалено удаление объекта 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.
Ответы
Ответ 1
При использовании SQLAlchemy в приложении Flask с 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__()
С помощью этой общей функции каждая модель, которая нуждается в коде для удаления-фиксации, теперь просто должна иметь метод __commit_delete__(self)
и делать все, что ему нужно сделать в этом методе.
Это также можно сделать без Flask-SQLAlchemy, однако в этом случае ему нужен еще один код:
- При выполнении записи необходимо выполнить удаление. Это делается с помощью
after_delete
event.
- Любые записанные удаления необходимо обрабатывать, когда COMMIT будет успешным. Это делается с помощью
after_commit
event.
- Если транзакция завершается неудачно или вручную откат, записанные изменения также необходимо очистить. Это делается с помощью события after_rollback().
Ответ 2
Это следует вместе с другими ответами на основе событий, но я думал, что я опубликую этот код, так как я написал его, чтобы решить вашу конкретную проблему:
Код (ниже) регистрирует класс 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
Ответ 3
это кажется немного сложным, мне любопытно, может ли SQL-ключ AFTER DELETE
быть лучшим маршрутом для этого, если он не будет сухим, и я не уверен, что база данных sql, которую вы используете, поддерживает ее, тем не менее AFAIK sqlalchemy выталкивает транзакции в db, но он действительно не знает, когда они совершаются, если Im правильно интерпретирует этот комментарий:
сам сервер базы данных, который поддерживает все "ожидающие" данные в текущей транзакции. Эти изменения не сохраняются постоянно на диске и публично отображаются в других транзакциях, пока база данных не получит команду COMMIT, которую передает Session.commit().
взято из SQLAlchemy: В чем разница между flush() и commit()? создателем sqlalchemy...
Ответ 4
Если ваш бэкэнд SQLAlchemy поддерживает его, включите двухфазную фиксацию. Вам понадобится использовать (или написать) модель транзакции для файловой системы, которая:
- проверяет разрешения и т.д., чтобы убедиться, что файл существует и может быть удален во время первой фазы фиксации.
- фактически удаляет файл во время второй фазы фиксации.
Это, вероятно, так хорошо, как это получится. Файловые системы Unix, насколько мне известно, не поддерживают XA или другие двухфазные транзакционные системы, поэтому вам придется жить с небольшой экспозицией из-за неожиданного сбоя файловой системы второй фазы.