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 <[email protected]> > 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 <[email protected]> > >> 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 <[email protected]> >> 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 <[email protected]> >> > >> >> 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 <[email protected]> >> >> 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 <[email protected]> >> >>> >> >>>> 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 < >> [email protected]> >> >>>> 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 <[email protected]> >> >>>>> >> >>>>>> 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 < >> [email protected] >> >>> >> >>>>>> 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 >> >>>>>> >> >>>>>> >> >>>> >> >>>> >> >> >> >> >> >> >
