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>