Pre- and post- put hooks for Datastore models

A number of people have asked about the possibility of pre- and post- put hooks for datastore models, to allow for changes or other processing before or after a model is stored to the datastore.

While such a feature isn't currently supported by App Engine, it's quite possible for us to implement it ourselves, using monkeypatching. This also gives us a good opportunity to show off how monkeypatching works, and how it can be used to make your own changes (at your own risk!) to the App Engine SDK.

One caveat of monkeypatching is that you have to be very careful to make sure that your patch is installed at all times. If it's not, the changes you made will be unavailable and cause errors - or worse, simply behave differently. This is particularly noticeable in the case of app-engine-patch, which monkeypatches models to change their kind name, causing operations on them to fail if the patch hasn't been imported.

The functionality we want is about as simple as you could ask for: We want to be able to define a method on our Model that gets called just before it is written to the datastore, and another method that is called just after. The natural way to implement this is to introduce our own Model subclass that should be subclassed by all models wanting this functionality, and doing it this way has a major advantage: It ensures that the monkeypatch is always installed when we're dealing with these models.

There are two separate avenues by which an entity can be stored to the datastore, so we need to cover both: The put() method on Model instances, and the db.put() method. Fortunately, since we're subclassing Model, we can take care of the first one ourselves.

Let's start by defining the model subclass:

class HookedModel(db.Model):
  def before_put(self):
    pass

  def after_put(self):
    pass

  def put(self, **kwargs):
    self.before_put()
    super(HookedModel, self).put(**kwargs)
    self.after_put()

That much was pretty obvious. Now, on to the actual monkeypatch, in order to handle db.put() properly.

The monkeypatch itself consists of 3 steps: Renaming the old version of the function we're replacing so we can still access it, defining our replacement, and replacing the original with our replacement. Without further ado, here it is, in that order:

old_put = db.put

def hooked_put(models, **kwargs):
  for model in models:
    if isinstance(model, HookedModel):
      model.before_put()
  old_put(models, **kwargs)
  for model in models:
    if isinstance(model, HookedModel):
      model.after_put()

db.put = hooked_put

There are a few tricks we could use to neaten up the hooked_put method, but we've left it the way it is - verbose - for clarity. That's all there is to it. Put both of those in the same module, import them wherever you need them, and otherwise use them as you would a normal module.

before_delete and after_delete methods, which would work similarly except for the necessity of being class methods, are left as an exercise to the reader - as is the possibility of allowing before/after methods to return additional objects to put or delete in the same batch.

Comments

blog comments powered by Disqus