cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
Subject svn commit: r1448525 - /cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml
Date Thu, 21 Feb 2013 07:02:28 GMT
Author: aadamchik
Date: Thu Feb 21 07:02:27 2013
New Revision: 1448525


lifecycle events


Modified: cayenne/main/trunk/docs/docbook/cayenne-guide/src/docbkx/lifecycle-events.xml
--- 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="" xmlns: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
+        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
+        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
+        in this chapter. When any such event occurs (e.g. an object is committed), Cayenne
+        invoke all appropriate callbacks. Persistent objects would receive their own events,
+        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
+        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
+                            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 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
+            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
+            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
+    }
+        <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
+            is all that is required at this point. </para>
+        <para>As a rule callback methods do not have any knowledge of the outside application,
+            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
+                    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
+                        there's a big downside to it. The power of the listeners lies in
+                        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
+                        Cayenne to instantiate the listeners severly limits mapping reusability.
+                        Further down in this chapter we'll assume that the listener classes
+                        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
+            callback methods. A callback method signature should be <code>void
+                someMethod(SomePersistentType object)</code>. It can be public, private,
+            or use default access:</para>
+            <para>
+                <programlisting language="java"> public class OrderListener { 
+   @PostAdd(Order.class)
+   public void setDefaultsForNewOrder(Order o) {
+      o.setCreatedOn(new Date());
+   }
+            </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
+            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
+            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
+    }
+        <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
+            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
+            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
+void postCommit(MyEntity1 object) {
+    ...
+        <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.
+            also another way to match entities - via custom annotations. This allows to match
+            number of entities without even knowing what they are. Here is an example. We'll
+            define a custom
+            annotation:<programlisting language="java">@Target(ElementType.TYPE)
+public @interface Tag {
+        <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
+    }
+        <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 {
+public class MyEntity2 extends _MyEntity2 {
+    <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
+            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
+            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]++;
+    }
+        <para>Now since this is both a filter and a listener, it needs to be registered
+            such:<programlisting language="java">CommittedObjectCounter counter = new
+ServerRuntime runtime = ...
+DataDomain domain = runtime.getDataDomain();
+// register filter
+// register listener

View raw message