Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run function after a certain type of model is committed

I want to run a function when instances of the Post model are committed. I want to run it any time they are committed, so I'd rather not explicitly call the function everywhere. How can I do this?

def notify_subscribers(post):
    """ send email to subscribers """
    ...

post = Post("Hello World", "This is my first blog entry.")
session.commit() # How to run notify_subscribers with post as argument
                 # as soon as post is committed successfully?

post.title = "Hello World!!1"
session.commit() # Run notify_subscribers once again.
like image 970
fnkr Avatar asked Sep 03 '15 14:09

fnkr


1 Answers

No matter which option you chose below, SQLAlchemy comes with a big warning about the after_commit event (which is when both ways send the signal).

The Session is not in an active transaction when the after_commit() event is invoked, and therefore can not emit SQL.

If your callback needs to query or commit to the database, it may have unexpected issues. In this case, you could use a task queue such as Celery to execute this in a background thread (with a separate session). This is probably the right way to go anyway, since sending emails takes a long time and you don't want your view to wait to return while it's happening.


Flask-SQLAlchemy provides a signal you can listen to that sends all the insert/update/delete ops. It needs to be enabled by setting app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True because tracking modifications is expensive and not needed in most cases.

Then listen for the signal:

from flask_sqlalchemy import models_committed

def notify_subscribers(app, changes):
    new_posts = [target for target, op in changes if isinstance(target, Post) and op in ('insert', 'update')]
    # notify about the new and updated posts

models_committed.connect(notify_subscribers, app)

app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True

You can also implement this yourself (mostly by copying the code from Flask-SQLAlchemy). It's slightly tricky, because model changes occur on flush, not on commit, so you need to record all changes as flushes occur, then use them after the commit.

from sqlalchemy import event

class ModelChangeEvent(object):
    def __init__(self, session, *callbacks):
        self.model_changes = {}
        self.callbacks = callbacks

        event.listen(session, 'before_flush', self.record_ops)
        event.listen(session, 'before_commit', self.record_ops)
        event.listen(session, 'after_commit', self.after_commit)
        event.listen(session, 'after_rollback', self.after_rollback)

    def record_ops(self, session, flush_context=None, instances=None):
        for targets, operation in ((session.new, 'insert'), (session.dirty, 'update'), (session.deleted, 'delete')):
            for target in targets:
                state = inspect(target)
                key = state.identity_key if state.has_identity else id(target)
                self.model_changes[key] = (target, operation)

    def after_commit(self, session):
        if self._model_changes:
            changes = list(self.model_changes.values())

            for callback in self.callbacks:
                callback(changes=changes)

            self.model_changes.clear()

    def after_rollback(self, session):
        self.model_changes.clear()
def notify_subscribers(changes):
    new_posts = [target for target, op in changes if isinstance(target, Post) and op in ('insert', 'update')]
    # notify about new and updated posts

# pass all the callbacks (if you have more than notify_subscribers)
mce = ModelChangeEvent(db.session, notify_subscribers)
# or you can append more callbacks
mce.callbacks.append(my_other_callback)
like image 128
davidism Avatar answered Nov 20 '22 10:11

davidism