cayenne-user mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Andrus Adamchik <and...@objectstyle.org>
Subject Re: Lifecycle Listeners / cayenne-3.1B1
Date Mon, 11 Mar 2013 07:07:09 GMT
I don't think there is a better way. Since Cayenne is only concerned with the state of object
persistent properties, modifying any such property is the only way to to get callbacks. "Phantom"
modifications (o.setX(o.getX()) or manually changing "persistenceState" are not going to cause
lifecycle events.

Andrus

On Mar 8, 2013, at 12:56 PM, Daniel Scheibe <dscheibe79@googlemail.com> wrote:
> Okay, found a workaround for now. I added another field "modificationId"
> which i modify on demand. This way the entity is really modified and
> presented in the callback. But i think there must be a better way...
> 
> Cheers,
> Daniel
> 
> 
> 2013/3/7 Daniel Scheibe <dscheibe79@googlemail.com>
> 
>> Andrus,
>> 
>> i'm making good progress but i have another quick question regarding the
>> persistency state of an entity instance. When i set a property on my
>> Content.class (inputStream) that is not a *real* field in (i.e.: mapped via
>> cayenne) can i somehow flag this entity instance as modified? For instance,
>> is it safe to mark the object as PersistenceState.MODIFIED from within when
>> i know there has been a change or is this causing strange side effects?
>> Otherwise it won't get recognized as a changed objects and i think the
>> "prePersist" lifecycle callback won't get called too...
>> 
>> Thanks in advance!
>> 
>> Cheers,
>> Daniel
>> 
>> 
>> 2013/3/7 Daniel Scheibe <dscheibe79@googlemail.com>
>> 
>>> Thanks Andrus,
>>> 
>>> i will do a prototype based on your suggestions (looks promising) to see
>>> if that works for me. I agree on not putting too much logic into the
>>> Persistent Objects. I just need to expose all of the functionality combined
>>> through a common interface on top of the actual cayenne/filestore
>>> implementation as the user of this shouldn't care about how the data is
>>> stored underneath the surface.
>>> 
>>> Basically it should be layered like this:
>>> 
>>> Persistency Layer (combines and abstract all the functionality underneath)
>>>    - Cayenne Layer
>>>    - Filestore Layer
>>> 
>>> Cheers,
>>> Daniel
>>> 
>>> 
>>> 
>>> 2013/3/7 Andrus Adamchik <andrus@objectstyle.org>
>>> 
>>>> Understood the scenario.
>>>> 
>>>> I usually stay away from too much logic like that in the objects
>>>> themselves and put that in some external handler classes, that can be
>>>> registered as listeners if needed. It is much easier (and cleaner) to
>>>> access application-specific "context" inside listeners that are not
>>>> persistent objects. A good example can be found in the "Combining Listeners
>>>> with DataChannelFilters" docs:
>>>> 
>>>> 
>>>> http://cayenne.apache.org/docs/3.1/cayenne-guide/lifecycle-events.html#comining-listeners-with-datachannelfilters
>>>> 
>>>> In your case you may also implement a DataChannelFilter that does stream
>>>> processing. E.g.:
>>>> 
>>>> public class StreamHandler implements DataChannelFilter {
>>>> 
>>>>    private ThreadLocal<Collection<InputStream>> streams;
>>>> 
>>>> 
>>>>    // this method collects streams that need to be processed in the
>>>> current transaction
>>>>    @PrePersist(entities = Content.class)
>>>>    @PreUpdate(entities = Content.class)
>>>>    @PreRemove(entities = Content.class)
>>>>    void beforeCommit(Content object) {
>>>>        streams.get().add(object.getStream());
>>>>    }
>>>> 
>>>>    @Override
>>>>    public void init(DataChannel channel) {
>>>>        streams = new ThreadLocal<Collection<InputStream>>();
>>>>    }
>>>> 
>>>>    @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) {
>>>> 
>>>>        // ignore all but commits
>>>>        if(syncType != FLUSH_CASCADE_SYNC) {
>>>>            return filterChain.onSync(originatingContext, changes,
>>>> syncType);
>>>>        }
>>>> 
>>>>        streams.set(new ArrayList<InputStream>());
>>>> 
>>>>        try {
>>>>            GraphDiff result = filterChain.onSync(originatingContext,
>>>> changes, syncType);
>>>> 
>>>>            // SUCCESS: handle streams
>>>>            return result;
>>>>        }
>>>>        catch(CayenneRuntimeException e) {
>>>>                // FAILURE: handle streams, rollback the context
>>>>                ...
>>>>                originatingContext.rollbackChanges();
>>>>        }
>>>>        finally {
>>>>            streams.set(null);
>>>>        }
>>>>    }
>>>> 
>>>> }
>>>> 
>>>> 
>>>> On Mar 6, 2013, at 6:51 PM, Daniel Scheibe <dscheibe79@googlemail.com>
>>>> wrote:
>>>>> Thanks for the quick response, Andrus!
>>>>> 
>>>>> I just came across the lines you just posted in "ObjectStore / void
>>>>> objectsRolledBack()" and i guess i'm starting to understand the
>>>> process a
>>>>> bit better now. I also agree that changing the behaviour of unsetting
>>>>> "objectContext" is not an option here.
>>>>> 
>>>>> Let me describe my use case a bit below:
>>>>> 
>>>>> I basically have a "Content" entity that reflects a File including the
>>>> name
>>>>> and the binary content. Now i have an implementation similar to this:
>>>>> 
>>>>> public class Content extends _Content
>>>>> {
>>>>>   private InputStream _inputStream = null;
>>>>> 
>>>>>   @Override
>>>>>   public void setData(String name, InputStream inputStream)
>>>>>   {
>>>>>       setName(name);
>>>>> 
>>>>>       _inputStream = inputStream;
>>>>>   }
>>>>> 
>>>>>   /* ... */
>>>>> }
>>>>> 
>>>>> Now i have the following scenarios below:
>>>>> 
>>>>> 1. commitChanges for "Content" entity instance in persistence state
>>>> "NEW"
>>>>>   - If an input stream was set, need to write the input stream to my
>>>> Http
>>>>> filestore in "prePersist"
>>>>> 
>>>>> 2. commitChanges fails and rollbackChanges is called for "Content"
>>>> entity
>>>>> instance in persistence state "NEW"
>>>>>   - If an input stream was written to the filestore in 1. i need to
>>>>> remove the previously written input stream from my Http filestore in
>>>>> "postRollback" (This is what i'm struggling with...)
>>>>> 
>>>>> 3. commitChanges for "Content" entity instance in persistence state
>>>>> "MODIFIED"
>>>>>   - If an input stream is already linked to this entity instance,
>>>>> remember it in "prePersist"
>>>>>   - If a new input stream was set, write the input stream to my Http
>>>>> filestore in "prePersist"
>>>>>   - Remove the previously remembered file input stream from my Http
>>>>> filestore in "postCommit"
>>>>> 
>>>>> 4. commitChanges for "Content" entity instance in persistence state
>>>>> "DELETED"
>>>>>   - If there is an input stream linked to this instance, remove it
>>>> from
>>>>> my Http filestore in "postCommit" or "postDelete"
>>>>> 
>>>>> Furthermore I store what i call "services" as user properties in the
>>>> object
>>>>> context instance via ObjectContext.setUserProperty("...") to access
>>>> these
>>>>> in my callbacks later on, for example the client wrapper to my Http
>>>>> filestore. Now in every callback method i grab the filestore client
>>>>> instance via ObjectContext.getUserProperty("...") to access it.
>>>>> 
>>>>> What i currently achieved so far is to integrate a callback into my
>>>>> application to let me know whenever an "Content" entity instance is in
>>>>> persistence state "NEW" and get's rolled back (scenario described in
>>>> 1. by
>>>>> implementing another workaround-lifecycle listener for "postRollback".
>>>>> 
>>>>> Unfortunately when this is called the object is in the persistence
>>>> state
>>>>> TRANSIENT and i no longer have access to the object context as it was
>>>> unset
>>>>> before.
>>>>> 
>>>>> So what i'm trying to achieve is to fully integrate the storage of a
>>>> binary
>>>>> file maintaining all aspects (lifecycle) of the "Content" entity and
>>>>> therefore need to make sure that whenever a commit was successfully
>>>>> executed the related file content was stored in the Http filestore. If
>>>>> something goes wrong somewhere in the middle (commitChanges() throws
an
>>>>> exception and i call rollbackChanges()) i try to undo/remove the file
>>>> from
>>>>> the Http filestore again if possible and only leaving back an orphaned
>>>>> (unlinked) file at worst.
>>>>> 
>>>>> I'm not worried about getting my "postRollback" workaround done but
>>>> maybe
>>>>> there is a better option to "inject" services into entities (not using
>>>> the
>>>>> ObjectContext as a workaround) or something?
>>>>> 
>>>>> I hope you get the idea here :)
>>>>> 
>>>>> Thanks in advance!
>>>>> 
>>>>> Cheers,
>>>>> Daniel
>>>>> 
>>>>> 2013/3/6 Andrus Adamchik <andrus@objectstyle.org>
>>>>> 
>>>>>> You are correct. The code that does it goes like this:
>>>>>> 
>>>>>> object.setObjectContext(null);
>>>>>> object.setObjectId(null);
>>>>>> object.setPersistenceState(PersistenceState.TRANSIENT);
>>>>>> 
>>>>>> I've been thinking about it recently. Rollback by definition wipes
>>>> out all
>>>>>> the uncommitted changes to the ObjectContext and its objects. I think
>>>> we
>>>>>> might preserve ObjectId property. Keeping it around does no harm,
even
>>>>>> though technically it is a side effect of the object previously being
>>>>>> registered with the context. We might change this behavior.
>>>>>> 
>>>>>> I am more hesitant to change the behavior unsetting "objectContext"
>>>>>> property. It appears to be a more consequential side effect.
>>>>>> 
>>>>>> I guess we just need an explicit rollback callback invoked prior
to
>>>>>> kicking the object out. I'll put it on the TODO list for 3.2. In
the
>>>>>> meantime I guess you'll have to implement some workaround. If you
>>>> describe
>>>>>> what your app does, we can think of a way to catch this.
>>>>>> 
>>>>>> Cheers,
>>>>>> Andrus
>>>>>> 
>>>>>> 
>>>>>> On Mar 6, 2013, at 4:58 PM, Daniel Scheibe <dscheibe79@googlemail.com
>>>>> 
>>>>>> wrote:
>>>>>>> Hi Andrus,
>>>>>>> 
>>>>>>> Exactly, i'm dealing with NEW objects. While trying to build
a
>>>> workaround
>>>>>>> for a "postRollback" callback i also noticed that the objects
are in
>>>> a
>>>>>>> TRANSIENT state after rollbackChanges() was executed. Unfortunately
>>>>>> objects
>>>>>>> in this state don't contain a lot of useful data anymore (ObjectId,
>>>>>>> ObjectContext is gone).
>>>>>>> 
>>>>>>> Cheers,
>>>>>>> Daniel
>>>>>>> 
>>>>>>> 
>>>>>>> 2013/3/6 Andrus Adamchik <andrus@objectstyle.org>
>>>>>>> 
>>>>>>>> Haven't tried to run it yet, but rolling back NEW object
transfers
>>>> them
>>>>>>>> into the TRANSIENT state. For this state change postLoad
(or any
>>>> other
>>>>>>>> callback) is indeed not invoked. It will be called for rolled
back
>>>>>> MODIFIED
>>>>>>>> and DELETED objects.
>>>>>>>> 
>>>>>>>> So in your application, are you dealing with reverting NEW
objects
>>>> too?
>>>>>>>> 
>>>>>>>> Andrus
>>>>>>>> 
>>>>>>>> On Mar 6, 2013, at 2:16 PM, Daniel Scheibe <
>>>> dscheibe79@googlemail.com>
>>>>>>>> wrote:
>>>>>>>> 
>>>>>>>>> Hi Andrus,
>>>>>>>>> 
>>>>>>>>> thanks for your feedback. I tried to dig into the Cayenne
source
>>>> code
>>>>>>>> from
>>>>>>>>> the trunk to see if there actually already is a test
case for my
>>>>>>>> scenario.
>>>>>>>>> Unfortunately i wasn't able to find one so i tried to
build one
>>>> myself
>>>>>>>>> (should make it fairly easy to test). Please find my
test case
>>>> below (I
>>>>>>>>> hope it is correct as i'm not yet familiar with the Cayenne
source
>>>> code
>>>>>>>>> structure)
>>>>>>>>> 
>>>>>>>>> So here is the code (i implemented it in
>>>>>>>>> org.apache.cayenne.access.DataContextCallbacksTest):
>>>>>>>>> 
>>>>>>>>> public void testPostLoadCallbacks() {
>>>>>>>>> 
>>>>>>>>>     LifecycleCallbackRegistry registry = runtime
>>>>>>>>>             .getDataDomain()
>>>>>>>>>             .getEntityResolver()
>>>>>>>>>             .getCallbackRegistry();
>>>>>>>>> 
>>>>>>>>>     // no callbacks
>>>>>>>>>     Artist a1 = context.newObject(Artist.class);
>>>>>>>>>     assertTrue(a1.getPostLoaded() == 0);
>>>>>>>>> 
>>>>>>>>>     try {
>>>>>>>>>         context.commitChanges();
>>>>>>>>>     } catch (CayenneRuntimeException cre) {
>>>>>>>>>         context.rollbackChanges();
>>>>>>>>>         assertTrue(a1.getPostLoaded() == 0);
>>>>>>>>>     }
>>>>>>>>> 
>>>>>>>>>     registry
>>>>>>>>>             .addListener(LifecycleEvent.POST_LOAD, Artist.class,
>>>>>>>>> "postLoadCallback");
>>>>>>>>> 
>>>>>>>>>     Artist a2 = context.newObject(Artist.class);
>>>>>>>>>     assertTrue(a2.getPostLoaded() == 0);
>>>>>>>>> 
>>>>>>>>>     try {
>>>>>>>>>         context.commitChanges();
>>>>>>>>>     } catch (CayenneRuntimeException cre) {
>>>>>>>>>         context.rollbackChanges();
>>>>>>>>>         assertTrue(a2.getPostLoaded() > 0);
>>>>>>>>>     }
>>>>>>>>> }
>>>>>>>>> 
>>>>>>>>> Should this test pass successfully or did i do something
wrong
>>>> here (I
>>>>>>>>> assume a2.getPostLoaded() should return a non-zero value
after a
>>>>>>>> rollback)?
>>>>>>>>> 
>>>>>>>>> Thanks in advance!
>>>>>>>>> 
>>>>>>>>> Cheers,
>>>>>>>>> Daniel
>>>>>>>>> 
>>>>>>>>> 
>>>>>>>>> 
>>>>>>>>> 2013/3/6 Andrus Adamchik <andrus@objectstyle.org>
>>>>>>>>> 
>>>>>>>>>> Hi Daniel,
>>>>>>>>>> 
>>>>>>>>>> Yes, post load callback should be invoked as advertised.
I never
>>>>>>>>>> personally tried it from a commit catch block, but
it should
>>>> work. Do
>>>>>>>> you
>>>>>>>>>> have a code sample? Maybe there is a scenario that
we do not
>>>> handle.
>>>>>>>>>> 
>>>>>>>>>>> Is there any chance to get a kind of a "postRollback"
lifecycle
>>>>>>>> callback
>>>>>>>>>>> working or something similar?
>>>>>>>>>> 
>>>>>>>>>> The original callbacks were taken from the JPA spec
that doesn't
>>>>>> specify
>>>>>>>>>> postRollback. We've already diverged from JPA by
adding PostAdd. I
>>>>>>>> think we
>>>>>>>>>> might go further to better reflect Cayenne object
lifecycle. So I
>>>> am
>>>>>>>> open
>>>>>>>>>> to adding PostRollback in the future (need to think
it through
>>>>>> though)…
>>>>>>>>>> 
>>>>>>>>>> Andrus
>>>>>>>>>> 
>>>>>>>>>> On Mar 5, 2013, at 6:43 PM, Daniel Scheibe <
>>>> dscheibe79@googlemail.com
>>>>>>> 
>>>>>>>>>> wrote:
>>>>>>>>>>> All,
>>>>>>>>>>> 
>>>>>>>>>>> i'm trying to get the lifecycle listeners working
for my use
>>>> case and
>>>>>>>>>> i've
>>>>>>>>>>> come accross a problem. I registered a listener
to do some extra
>>>>>> stuff
>>>>>>>>>> for
>>>>>>>>>>> an entity whenever it will be persisted (prePersist)
via:
>>>>>>>>>>> 
>>>>>>>>>>> callbackRegistry.addListener(LifecycleEvent.PRE_PERSIST,
>>>>>> Content.class,
>>>>>>>>>>> "prePersist");
>>>>>>>>>>> 
>>>>>>>>>>> This get's called as expected and works smoothly.
>>>>>>>>>>> 
>>>>>>>>>>> Now whenever i have the scenario of a CommitException
thrown
>>>> during
>>>>>>>>>>> commitChanges() (for whatever reason, underlying
database not
>>>>>>>> available,
>>>>>>>>>>> etc.) i need to revert some of the stuff i did
in the
>>>> "prePersist"
>>>>>>>>>>> lifefycle callback on the object in question.
>>>>>>>>>>> 
>>>>>>>>>>> Unfortunately i haven't had luck yet to register
a lifecycle
>>>> listener
>>>>>>>>>> that
>>>>>>>>>>> will be called in case of a "rollback" through
"rollbackChanges".
>>>>>>>>>>> 
>>>>>>>>>>> The documentation states something about "PostLoad"
being called
>>>>>>>> "Within
>>>>>>>>>>> "ObjectContext.rollbackChanges()" after the object
is reverted."
>>>>>>>>>> (although
>>>>>>>>>>> this is from 3.0 i guess it should still apply?
>>>>>>>>>>> https://cayenne.apache.org/docs/3.0/lifecycle-callbacks.html)
>>>>>>>>>>> 
>>>>>>>>>>> Is there any chance to get a kind of a "postRollback"
lifecycle
>>>>>>>> callback
>>>>>>>>>>> working or something similar? Or did i just hit
a bug with the
>>>>>> version
>>>>>>>>>> i'm
>>>>>>>>>>> using?
>>>>>>>>>>> 
>>>>>>>>>>> Any help is much appreciated.
>>>>>>>>>>> 
>>>>>>>>>>> Cheers,
>>>>>>>>>>> Daniel
>>>>>>>>>> 
>>>>>>>>>> 
>>>>>>>> 
>>>>>>>> 
>>>>>> 
>>>>>> 
>>>> 
>>>> 
>>> 
>> 


Mime
View raw message