Community contributed extensions

Handle model lifecycle events

The content of this chapter is only valid for PlayMorphia v1.2.4 or later.

Atomic update operations (new in v1.2.6) does not support lifecycle events yet.

A model could experience different lifecycle events, e.g. loading from MongoDB, saving to MongoDB and deleting from MongoDB. It would be interesting for application developer to listening to these events and weave in certain logic when a specific event triggered. Typical usage senarios includes updating modified timestamp before saving model object to MongoDB, or logging updating to accouting database if certain model entity is been modified.

PlayMorphia provides a set of tools to enable application developer coding to the model lifecycle events. There are two set of lifecycle events supported by PlayMorphia:

  1. Morphia lifecycle events
  2. PlayMorphia lifecycle events

Both Morphia and PlayMorphia provides two mechanism 1 for application developer to inject logic when specific entity lifecycle events triggerred:

  1. Define event handling methods in the model class
  2. Define event handling methods in a separate class

1 PlayMorphia application developer could also use the Morphia’s lifecycle event handling mechanism without any problem.

Define lifecycle event handling methods in the model class

A handy way to bind lifecycle event listener to your model is to mark your model methods with lifecycle event annotations:

@Entity public class User extends Model {
    public String username;
    public String password;
    public long created;
    public long modified;
    @OnAdd void fillInCreatedTimestamp() {
        created = System.currentTimeMills();
    }
    @OnUpdate void fillInCreatedTimestamp() {
        modified = System.currentTimeMills();
    }
}

The above example use PlayMorphia lifecycle annotation @OnAdd and @OnUpdate to set created and modified timestamp of the model. You could also achieve the same result using Morphia annotation:

@Entity public class User extends Model {
    public String username;
    public String password;
    public long created;
    public long modified;
    @PrePersist void fillInTimestamp() {
      if (isNew()) {
          created = System.currentTimeMills();
      } else {
          modified = System.currentTimeMills();
      }
    }
}

The above samples are listed here for demo purpose, you don’t need to do the same thing in your application. To add timestamp to your mode, just annotate your model class with AutoTimestamp. See here for detail.

Define a separate event handler class to handle model(s) lifecycle events

If you have common logic for event handling of multiple models you don’t want to copy and paste the the from one model class to another one. You could implement a common event handler listen to all model lifecycle events

PlayMorphia life cycle event handler

With PlayMorphia you could create a class that implement play.modules.morphia.MorphiaEvent.IMorphiaEventHandler interface to define the common logic handling model entity lifecycle events:

public class MyEntityEventHandler implements IMorphiaEventHandler {
    ...
    public void added(Model context) {
        Logger.info("%s created", context.getClass());
    }
    ...
}

There are 10 methods defined in IMorphiaEventHandler interface. If you don’t want to implement all of them, make your class extend play.modules.morphia.MorphiaEvent.IMorphiaEventHandler class instead and override only methods you are interested in:

public class MyEntityEventHandler extends MorphiaEventHandlerAdaptor {
    public void added(Model context) {
        Logger.info("%s created", context.getClass());
    }
}

Register PlayMorphia event handler

Once you defined the event handler you can bind it to watch the lifecycle events of one more more PlayMorphia models. There are 2 ways to do this:

  1. Call registration APIs of MorphiaPlugin
  2. Using PlayMorphia’s automatic registration mechanism with annotations
MorphiaPlugin Registration API

Once you’ve defined event handler class, you could register it to watch the lifecycle events of all models:

MyEntityEventHandler h = new MyEntityEventHandler();
MorphiaPlugin.registerGlobalEventHandler(h);

Or you can make the event handler to process lifecycle events of only certain models:

MyEntityEventHandler h = new MyEntityEventHandler();
MorphiaPlugin.registerModelEventHandler(User.class, h);
MorphiaPlugin.registerModelEventHandler(Account.class, h);
Automatic registration

In addition to registration API, PlayMorphia provides a better way to register event handler: automatic registrater event lister by using annotations.

@Watch
public class MyEntityEventHandler extends MorphiaEventHandlerAdaptor {
    public void added(Model context) {
        Logger.info("%s created", context.getClass());
    }
}

The above code marked MyEntityEventHandler with @Watch annotation, which tell PlayMorphia to register MyEntityEventHandler to watch the lifecyle event of all models. You can add argument to the @Watch annotation to make it watch given models only:

@Watch({User.class, Account.class}) 
public class UserAccountEventListener implements IMorphiaEventHandler {
    ...
    public void added(Model context) {
        Logger.info("%s created", context.getClass());
    }
    public void updated(Model context) {
        Logger.info("%s modified", context.getClass());
    }
}

As a counter part of @Watch annotation, PlayMorphia also provides a @WatchBy annotation, which could be used to annotate the model class to define which event handlers will be registered to watch the lifecycle events of the annotated model:

@WatchBy({MyEventHandler.class, YourEventHandler.class})
@Entity
public class MyModel extends Model {
    ...
}

The above code dictate that the lifecycle events of MyModel will be watched by both MyEventHandler and YourEventHandler.

Event handler automatic registration rules

  1. The auto-registration only effects when the morphia.autoRegisterEventHandler configuration is turned on in the application.conf file. In other words if morphia.autoRegisterEventHandler is turned off then @Watch and @WatchBy has no effect. By default the morphia.autoRegisterEventHandler configuration is ON.
  2. Although you can put any class in the @Watch annotation parameter, however only class that extends play.modules.morphia.Model will effective.
  3. If Model.class has been passed to @Watch annotation, then the annotated event listener class will be registered to listen to the events of all application models
  4. If a super model class has been passed to @Watch annotation, then the super model class and all it’s descendent model classes will be registered to be watched by the event handler class unless they are abstract. Similarly, if a super model class has been annotated with @WatchBy, then the event handlers declared in the @WatchBy annotation will be registered to watch the lifecycle events of the super model and all it’s descendants unless they are abstract.
  5. If an event handler class is referenced at different place, it might registered multiple times and it’s event handling methods might be called multiple times when one event triggered. E.g
    1. Event handler class H is annotated with @Watch({Model.class, MyModel.class}), it will be registered to listen to the global model events as well as the MyModel events. Thus when an MyModel entity lifecycle event triggered, H's event handling methods will be called twice
    2. Event handler class B is annotated with @Watch(MyModel.class), and MyModel class is annotated with @WatchBy(B.class), then the B handler will be registered twice for MyModel model class. And anytime MyModel lifecycle event triggered, the relevant method of B will be called twice.

Play Plugin event dispatch interface

In addition to the Morphia event handler interface, Plugin developers can also get access to entity lifecycle events via the standard Play plugin event interface.

public class MyPlugin extends PlayPlugin {
    ...
    @Override public void onEvent(String message, Object context) {
        if (MorphiaEvent.UPDATED.toString().equals(message)) {
            Model model = (Model)context;
            // handle updated event on model
            ...
        }
    }
}

However it is recommended to use the IMorphiaEventHandler interface instead of play plugin event interface because

  1. You get strong type object for event rather than a plain string
  2. You get event dispatch automatically 1 rather than writing a series of if-else clause in the plugin’s onEvent handler

1 Automatic event dispatch only available for event handlers that are located in module’s app folder. If you have created event handler in the src folder and compiled in a jar file, you will need to call the registration API to register your event handler.

For the sake of performance, PlayMorphia disable plugin event dispatch by default. To enable plugin event dispatch, application developer needs to add morphia.postPluginEvent=true in the conf/application.conf file.

Morphia Entity Listener

Morphia library also provides a lifecycle event dispatch mechanism. Here is the Morphia version of UserAccountEventListener:

public class UserAccountEventListener {
    @PrePersis void prePersist(Model context) {
        if (context.isNew()) {
            Logger.info("%s created", context.getClass());
        } else {
            Logger.info("%s modified", context.getClass());
        }
    }
}

To make the above event listener listen to User and Account lifecycle events you need to annotate the corresponding Model class with @com.google.code.morphia.annotations.EntityListeners:

@EntityListeners(UserAccountEventListener.class)
@Entity public class User extends Model {...}
@EntityListeners(UserAccountEventListener.class)
@Entity public class Account extends Model {...}

As you can see it is pretty much the same thing as PlayMorphia’s @WatchBy annotation. However the class passed into @EntityListeners annotation is not PlayMorphia event handler, instead the developer needs to use the Morphia lifecycle annotation to mark the lifecycle event handling methods.

For more about Morphia entity listener interface, please check here for detail.

Note you can choose either PlayMorphia or Morphia lifecycle event dispatch mechanism. However it’s suggested that you stick to PlayMorphia one because:

  1. PlayMorphia defines clearer and more event types
  2. PlayMorphia defines common event handler at global and model level
  3. With PlayMorphia, you can define handler/model binding relationship by annotating on either Model (@WatchBy) or Handler (@Watch), while Morphia force you to annotate on Model class only (@EntityListeners)

PlayMorphia lifecycle event handler (annotation based or interface based) will ONLY BE CALLED when you are operating on PlayMorphia classes. They will NOT get triggered when you are manupulating the low level objects, e.g. If you save/load/delete MongoDB’s DBObject directly the added/updated/loaded events will not trigger, if you call delete() on com.google.code.morphia.query.Query, the onBatchDelete and batchDeleted events will not trigger.

Resources

  1. Lifecyle model of PlayMorphia and Morphia
  2. Introduction to PlayMorphia model
  3. Use PlayMorphia model: crud