You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2013/02/21 08:02:28 UTC
svn commit: r1448525 -
/cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml
Author: aadamchik
Date: Thu Feb 21 07:02:27 2013
New Revision: 1448525
URL: http://svn.apache.org/r1448525
Log:
docs
lifecycle events
Modified:
cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml
Modified: cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml
URL: http://svn.apache.org/viewvc/cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml?rev=1448525&r1=1448524&r2=1448525&view=diff
==============================================================================
--- cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml (original)
+++ cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml Thu Feb 21 07:02:27 2013
@@ -2,19 +2,317 @@
<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink"
version="5.0" xml:id="lifecycle-events">
<title>Lifecycle Events</title>
+ <para>An application might be interested in getting notified when a Persistent object moves
+ through its lifecycle (i.e. fetched from DB, created, modified, committed). E.g. when a new
+ object is created, the application may want to initialize its default properties (this can't
+ be done in constructor, as constructor is also called when an object is fetched from DB).
+ Before save, the application may perform validation and/or set some properties (e.g.
+ "updatedTimestamp"). After save it may want to create an audit record for each saved object,
+ etc., etc. </para>
+ <para>All this can be achieved by declaring callback methods either in Persistent objects or in
+ non-persistent listener classes defined by the application (further simply called
+ "listeners"). There are eight types of lifecycle events supported by Cayenne, listed later
+ in this chapter. When any such event occurs (e.g. an object is committed), Cayenne would
+ invoke all appropriate callbacks. Persistent objects would receive their own events, while
+ listeners would receive events from any objects. </para>
+ <para>Cayenne allows to build rather powerful and complex "workflows" or "processors" tied to
+ objects lifecycle, especially with listeners, as they have full access to the application
+ evnironment outside Cayenne. This power comes from such features as filtering which entity
+ events are sent to a given listener and the ability to create a common operation context for
+ multiple callback invocations. All of these are discussed later in this chapter.</para>
<section xml:id="types-of-lifecycle-events">
<title>Types of Lifecycle Events</title>
+ <para>Cayenne defines the following 8 types of lifecycle events for which callbacks can be
+ regsitered:<table frame="void">
+ <caption>Lifecycle Event Types</caption>
+ <col width="16%"/>
+ <col width="84%"/>
+ <thead>
+ <tr>
+ <th>Event</th>
+ <th>Occurs...</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>PostAdd</td>
+ <td>right after a new object is created inside
+ <code>ObjectContext.newObject()</code>. When this event is fired the
+ object is already registered with its ObjectContext and has its ObjectId
+ and ObjectContext properties set.</td>
+ </tr>
+ <tr>
+ <td>PrePersist</td>
+ <td>right before a new object is committed, inside
+ <code>ObjectContext.commitChanges()</code> and
+ <code>ObjectContext.commitChangesToParent()</code> (and prior to
+ "<code>validateForInsert()</code>").</td>
+ </tr>
+ <tr>
+ <td>PreUpdate</td>
+ <td>right before a modified object is committed, inside
+ <code>ObjectContext.commitChanges()</code> and
+ <code>ObjectContext.commitChangesToParent()</code> (and prior to
+ "<code>validateForUpdate()</code>").</td>
+ </tr>
+ <tr>
+ <td>PreRemove</td>
+ <td>right before an object is deleted, inside
+ <code>ObjectContext.deleteObjects()</code>. The event is also
+ generated for each object indirectly deleted as a result of CASCADE
+ delete rule.</td>
+ </tr>
+ <tr>
+ <td>PostPersist</td>
+ <td>right after a commit of a new object is done, inside
+ <code>ObjectContext.commitChanges()</code>.</td>
+ </tr>
+ <tr>
+ <td>PostUpdate</td>
+ <td>right after a commit of a modified object is done, inside
+ <code>ObjectContext.commitChanges()</code>.</td>
+ </tr>
+ <tr>
+ <td>PostRemove</td>
+ <td>right after a commit of a deleted object is done, inside
+ <code>ObjectContext.commitChanges()</code>.</td>
+ </tr>
+ <tr>
+ <td>PostLoad</td>
+ <td>
+ <itemizedlist>
+ <listitem>
+ <para>After an object is fetched inside
+ <code>ObjectContext.performQuery()</code>.</para>
+ </listitem>
+ <listitem>
+ <para>After an object is reverted inside
+ <code>ObjectContext.rollbackChanges()</code>.</para>
+ </listitem>
+ <listitem>
+ <para>Anytime a faulted object is resolved (i.e. if a
+ relationship is fetched).</para>
+ </listitem>
+ </itemizedlist>
+ </td>
+ </tr>
+ </tbody>
+ </table></para>
</section>
- <section xml:id="lifecycle-callbacks-listeners">
- <title>Lifecycle Callbacks and Listeners</title>
- <section xml:id="callback-listener-method-semantics">
- <title>Callback and Listener Methods Semantics</title>
- </section>
- <section xml:id="registering-callbacks-listeners">
- <title>Registering Callbacks and Listeners</title>
- </section>
- <section xml:id="comining-listeners-with-datachannelfilters">
- <title>Combining Listeners with DataChannelFilters</title>
+ <section xml:id="callback-persistent">
+ <title>Callbacks on Persistent Objects</title>
+ <para>Callback methods on Persistent classes are mapped in CayenneModeler for each
+ ObjEntity. Empty callback methods are automatically created as a part of class
+ generation (either with Maven, Ant or the Modeler) and are later filled with appropriate
+ logic by the programmer. E.g. assuming we mapped a 'post-add' callback called
+ 'onNewOrder' in ObjEntity 'Order', the following code will be
+ generated:<programlisting language="java">public abstract class _Order extends CayenneDataObject {
+ protected abstract void onNewOrder();
+}
+
+public class Order extends _Order {
+
+ @Override
+ protected void onNewOrder() {
+ //TODO: implement onNewOrder
+ }
+}</programlisting></para>
+ <para>As <code>onNewOrder()</code> is already declared in the mapping, it does not need to
+ be registered explicitly. Implementing the method in subclass to do something meaningful
+ is all that is required at this point. </para>
+ <para>As a rule callback methods do not have any knowledge of the outside application, and
+ can only access the state of the object itself and possibly the state of other
+ persistent objects via object's own ObjectContext.</para>
+ <para>
+ <note>
+ <para><emphasis role="italic">Validation and callbacks:</emphasis> There is a clear
+ overlap in functionality between object callbacks and
+ <code>DataObject.validateForX()</code> methods. In the future validation may
+ be completely superceeded by callbacks. It is a good idea to use "validateForX"
+ strictly for validation (or not use it at all). Updating the state before commit
+ should be done via callbacks.</para>
+ </note>
+ </para>
+ </section>
+ <section xml:id="callback-non-persistent">
+ <title>Callbacks on Non-Persistent Listeners</title>
+
+ <para>
+ <note>
+ <para>While listener callback methods can be declared in the Modeler (at least
+ as of this wrting), which ensures their automatic registration in runtime,
+ there's a big downside to it. The power of the listeners lies in their
+ complete separation from the XML mapping. The mapping once created, can be
+ reused in different contexts each having a different set of listeners.
+ Placing a Java class of the listener in the XML mapping, and relying on
+ Cayenne to instantiate the listeners severly limits mapping reusability.
+ Further down in this chapter we'll assume that the listener classes are
+ never present in the DataMap and are registered via API.</para>
+ </note>
+ </para>
+ <para>A listener is simply some application class that has one or more annotated
+ callback methods. A callback method signature should be <code>void
+ someMethod(SomePersistentType object)</code>. It can be public, private, protected
+ or use default access:</para>
+ <para>
+ <programlisting language="java"> public class OrderListener {
+
+ @PostAdd(Order.class)
+ public void setDefaultsForNewOrder(Order o) {
+ o.setCreatedOn(new Date());
+ }
+}</programlisting>
+ </para>
+ <para>Notice that the example above contains an annotation on the callback method that
+ defines the type of the event this method should be called for. Before we go into
+ annotation details, we'll show how to create and register a listener with Cayenne. It is
+ always a user responsibility to register desired application listeners, usually right
+ after ServerRuntime is started. Here is an example:</para>
+ <para>First let's define 2 simple
+ listeners.<programlisting language="java">public class Listener1 {
+
+ @PostAdd(MyEntity.class)
+ void postAdd(Persistent object) {
+ // do something
+ }
+}
+
+public class Listener2 {
+
+ @PostRemove({ MyEntity1.class, MyEntity2.class })
+ void postRemove(Persistent object) {
+ // do something
+ }
+
+ @PostUpdate({ MyEntity1.class, MyEntity2.class })
+ void postUpdate(Persistent object) {
+ // do something
+ }
+}</programlisting></para>
+ <para>Ignore the annotations for a minute. The important point here is that the listeners
+ are arbitrary classes unmapped and unknown to Cayenne, that contain some callback
+ methods. Now let's register them with
+ runtime:<programlisting language="java">ServerRuntime runtime = ...
+
+LifecycleCallbackRegistry registry =
+ runtime.getDataDomain().getEntityResolver().getCallbackRegistry();
+
+registry.addListener(new Listener1());
+registry.addListener(new Listener2());</programlisting></para>
+ <para>Listeners in this example are very simple. However they don't have to be. Unlike
+ Persistent objects, normally listeners initialization is managed by the application
+ code, not Cayenne, so listeners may have knowledge of various application services,
+ operation transactional context, etc. Besides a single listener can apply to multiple
+ entities. As a consequence their callbacks can do more than just access a single
+ ObjectContext. </para>
+ <para>Now let's discuss the annotations. There are eight annotations exactly matching the
+ names of eight lifecycle events. A callback method in a listener should be annotated
+ with at least one, but possibly with more than one of them. Annotation itself defines
+ what event the callback should react to. Annotation parameters are essentially an entity
+ filter, defining a subset of ObjEntities whose events we are interested
+ in:<programlisting language="java">// this callback will be invoked on PostRemove event of any object
+// belonging to MyEntity1, MyEntity2 or their subclasses
+@PostRemove({ MyEntity1.class, MyEntity2.class })
+void postRemove(Persistent object) {
+ ...
+}</programlisting><programlisting language="java">// similar example with multipe annotations on a single method
+// each matching just one entity
+@PostPersist(MyEntity1.class)
+@PostRemove(MyEntity1.class)
+@PostUpdate(MyEntity1.class)
+void postCommit(MyEntity1 object) {
+ ...
+}</programlisting></para>
+ <para>As shown above, "value" (the implicit annotation parameter) can contain one or more
+ entity classes. Only these entities' events will result in callback invocation. There's
+ also another way to match entities - via custom annotations. This allows to match any
+ number of entities without even knowing what they are. Here is an example. We'll first
+ define a custom
+ annotation:<programlisting language="java">@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Tag {
+
+}</programlisting></para>
+ <para>Now we can define a listener that will react to events from ObjEntities annotated with
+ this
+ annotation:<programlisting language="java">public class Listener3 {
+
+ @PostAdd(entityAnnotations = Tag.class)
+ void postAdd(Persistent object) {
+ // do something
+ }
+}</programlisting></para>
+ <para>As you see we don't have any entities yet, still we can define a listener that does
+ something useful. Now let's annotate some
+ entities:<programlisting language="java">@Tag
+public class MyEntity1 extends _MyEntity1 {
+
+}
+
+@Tag
+public class MyEntity2 extends _MyEntity2 {
+
+}</programlisting></para>
</section>
+
+ <section xml:id="comining-listeners-with-datachannelfilters">
+ <title>Combining Listeners with DataChannelFilters</title>
+ <para>A final touch in the listeners design is preserving the state of the listener within a
+ single select or commit, so that events generated by multiple objects can be collected
+ and processed all together. To do that you will need to implements a
+ <code>DataChannelFilter</code>, and add some callback methods to it. They will store
+ their state in a ThreadLocal variable of the filter. Here is an example filter that does
+ something pretty meaningless - counts how many total objects were committed. However it
+ demonstrates the important pattern of aggregating multiple events and presenting a
+ combined
+ result:<programlisting language="java">public class CommittedObjectCounter implements DataChannelFilter {
+
+ private ThreadLocal<int[]> counter;
+
+ @Override
+ public void init(DataChannel channel) {
+ counter = new ThreadLocal<int[]>();
+ }
+
+ @Override
+ public QueryResponse onQuery(ObjectContext originatingContext, Query query, DataChannelFilterChain filterChain) {
+ return filterChain.onQuery(originatingContext, query);
+ }
+
+ @Override
+ public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType,
+ DataChannelFilterChain filterChain) {
+
+ // init the counter for the current commit
+ counter.set(new int[1]);
+
+ try {
+ return filterChain.onSync(originatingContext, changes, syncType);
+ } finally {
+
+ // process aggregated result and release the counter
+ System.out.println("Committed " + counter.get()[0] + " object(s)");
+ counter.set(null);
+ }
+ }
+
+ @PostPersist(entityAnnotations = Tag.class)
+ @PostUpdate(entityAnnotations = Tag.class)
+ @PostRemove(entityAnnotations = Tag.class)
+ void afterCommit() {
+ counter.get()[0]++;
+ }
+}</programlisting></para>
+ <para>Now since this is both a filter and a listener, it needs to be registered as
+ such:<programlisting language="java">CommittedObjectCounter counter = new CommittedObjectCounter();
+
+ServerRuntime runtime = ...
+DataDomain domain = runtime.getDataDomain();
+
+// register filter
+domain.addFilter(counter);
+
+// register listener
+domain.getEntityResolver().getCallbackRegistry().addListener(counter);</programlisting></para>
</section>
</chapter>