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:
- Morphia lifecycle events
- PlayMorphia lifecycle events
Both Morphia and PlayMorphia provides two mechanism 1 for application developer to inject logic when specific entity lifecycle events triggerred:
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:
- Call registration APIs of
MorphiaPlugin
- 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
- The auto-registration only effects when the
morphia.autoRegisterEventHandler
configuration is turned on in theapplication.conf
file. In other words ifmorphia.autoRegisterEventHandler
is turned off then@Watch
and@WatchBy
has no effect. By default themorphia.autoRegisterEventHandler
configuration is ON. - Although you can put any class in the
@Watch
annotation parameter, however only class that extendsplay.modules.morphia.Model
will effective. - 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 - 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. - 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
- 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 theMyModel
events. Thus when anMyModel
entity lifecycle event triggered,H
's event handling methods will be called twice - Event handler class
B
is annotated with@Watch(MyModel.class)
, andMyModel
class is annotated with@WatchBy(B.class)
, then theB
handler will be registered twice forMyModel
model class. And anytimeMyModel
lifecycle event triggered, the relevant method ofB
will be called twice.
- Event handler class
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
- You get strong type object for event rather than a plain string
- 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:
- PlayMorphia defines clearer and more event types
- PlayMorphia defines common event handler at global and model level
- 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.