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&lt;int[]> counter;
+
+    @Override
+    public void init(DataChannel channel) {
+        counter = new ThreadLocal&lt;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>