You are viewing a plain text version of this content. The canonical link for it is here.
Posted to user@cayenne.apache.org by edward pedersson <cp...@googlemail.com> on 2007/01/05 17:15:39 UTC

nested dataContexts

Hi

I am using the latest version of Cayenne with Tapestry 4.0.1.

I am having a bit of trouble using nested dataContexts. I would like the
user to be able to create a new object in a wizrd style application but have
the option to cancel half way through the stages.

I have tried a few different options and noe seem to work. In the first
instance I tried the nested datacontext like this:

AClass parent; //assume this exists and is in the threaded context

DataContext childContext = this.getDataContext().createChildContext() //
this.getDataContext() is the threaded context

AnotherClass newObject =
(AnotherClass)childContext.createAndRegisterNewObject(AnotherClass.class);
AClass localParentObject = (AClass)childContext.localObject(
parent.getObjectId(),null);

object.setParent(localParent);

//do other stuff here like set other properties

// now save to parent

childContext.commitChangesToParent();

For some reason I seem to get the parent having two children, not just the
one! I assume it is because the changes in the childContext are propagated
to the parent context so as a result the first setParent call adds the child
to the parent and on commit changes the child is added again to the parent.

So I tried to have separate dataContexts instead by doing this

DataContext anotherContext = DataContext.createDataContext()

AnotherClass newObject =
(AnotherClass)anotherContext.createAndRegisterNewObject(AnotherClass.class);
AClass localParentObject = (AClass)anotherContext.localObject(
parent.getObjectId(),null);

object.setParent(localParent);

//do other stuff here like set other properties

// now save to parent
newObject = (AClass)this.getDataContext().localObject(newObject.getObjectId
(),null);


I get errors like Can't build a query for relationship '...' for temporary
id:

Does anyone knew what I am doing wrong?

many thanks for your help.

-- 


-- e

Re: nested dataContexts

Posted by edward pedersson <cp...@googlemail.com>.
Thanks for replying Andrus. I cam up with a solution to the problem that I
will detail below. the problem only seemed to manifest itself when I was
using Tapestry. It may have to do with Tapestry holding on to objects in
memory ot manipulating them in some way that led to Cayenne assuming they
were in the same context. I can't be certain as it has been quite erratic
but consistently broken.

To get around it, I am creating a new context and I have implemented my own
version of localObject as such

    public static DataObject localDataObject(DataContext
dataContext,DataObject dataObject) {

        DataObject localDataObject = dataObject;

        if (dataContext != null && dataObject != null &&
!dataObject.getDataContext().equals(dataContext)) {

            // in most cases this just works
            localDataObject = (DataObject)dataContext.localObject(
dataObject.getObjectId(),null);

            //but for new objects in nested contexts it does not work and
adding a prototype to the call above returns
            //a committed dataObject which fails on a fetch as it is a
temporary id
            // so deep merge with the object we are trying to localise from
            if (dataObject.getObjectId().isTemporary() &&
localDataObject.getPersistenceState() == PersistenceState.HOLLOW) {
                CayenneUtil.deepMerge(dataObject,localDataObject);
            }

            //don't really need to do this but it did cause issues elsewhere
for some odd reason
            if (localDataObject.getPersistenceState() ==
PersistenceState.HOLLOW && !dataObject.getObjectId().isTemporary()) {
                dataContext.getObjectStore().resolveHollow(localDataObject);
            }
        }

        return localDataObject;
    }


And for some reason as you can see in the code above I was getting hollow
new dataObjects which would cause a fetch to the database and this is
completely wrong. So I have had to force a deepMerge of the object I am
trying to localise and these are the details of the method below

     /**
     * visit every property of fromDataObject and copy the values to
toDataContext making sure the values are in the proper dataContext
     *
     * @param fromDataObject
     * @param toDataObject
     */
    @SuppressWarnings("unchecked")
    public static void deepMerge(final DataObject fromDataObject, final
DataObject toDataObject) {

        ClassDescriptor descriptor = toDataObject.getDataContext
().getEntityResolver().getClassDescriptor(toDataObject.getObjectId
().getEntityName());

        //we want the localised object to have the same state as the other
        toDataObject.setPersistenceState(fromDataObject.getPersistenceState
());
        descriptor.injectValueHolders(toDataObject);

        descriptor.visitProperties(new PropertyVisitor() {

            public boolean visitCollectionArc(CollectionProperty property) {

                PropertyAccessor accessor = new DataObjectAccessor(
property.getName());
                ToManyList oldList =
(ToManyList)property.readProperty(fromDataObject);

                //lists are annoying. we need to make sure the owner of the
list is the new object which requires a copy
                ToManyList newList = new ToManyList(toDataObject,
oldList.getRelationship());
                Iterator<DataObject> it = oldList.iterator();

                //then we need to localise every object in the list as well
otherwise a real mess
                while (it.hasNext()) {
                    newList.add(CayenneUtil.localDataObject(
toDataObject.getDataContext(),it.next()));
                }

                accessor.writePropertyDirectly(toDataObject,
property.readProperty(toDataObject), newList);

                return true;
            }

            public boolean visitSingleObjectArc(SingleObjectArcProperty
property) {
                PropertyAccessor accessor = new DataObjectAccessor(
property.getName());
                Object newValue = property.readProperty(fromDataObject);

                //we need to to make sure relationship pull in objects from
the same context so recurse here
                if (newValue instanceof DataObject) {
                    newValue = CayenneUtil.localDataObject(
toDataObject.getDataContext(),(DataObject)newValue);
                }

                accessor.writePropertyDirectly(toDataObject,
property.readProperty(toDataObject), newValue);

                return true;
            }

            public boolean visitProperty(Property property) {
                PropertyAccessor accessor = new DataObjectAccessor(
property.getName());
                accessor.writePropertyDirectly(toDataObject,
property.readProperty(toDataObject), property.readProperty(fromDataObject));
                return true;
            }
        });
    }


And finally, because I am using a new dataContext, i want to merge the
changes back into the threaded context so I have done this

        newObject().getDataContext().setChannel(this.getDataContext()); //
this.getDataContext() is the threaded context

        CayenneUtil.syncDataContexts(this.getDataContext(),
newNode().getDataContext());

        newNode().getDataContext().commitChangesToParent();

Seems quite wrong setting the channel like this but it seems to work. The
only issue I had was the context failed to merge properly because the
objects I fetched in the new context did not exist in the context I was
trying to merge in to at commitToParent hence the call
CayenneUtil.syncDataContexts(...,...) above. The details are below

    /**
     * play with new contexts and add or remove objects or even fetch
objects and if you want to merge back
     * to another context you need to force the existing context to fetch
the existing objects as it won't
     * touch them in the merge
     *
     * @param existingDataContext
     * @param newDataContext
     */
    public static void syncDataContexts(DataContext existingDataContext,
DataContext newDataContext) {
        Iterator it = newDataContext.getGraphManager
().registeredNodes().iterator();

        while (it.hasNext()) {
            try {
                DataObjectUtils.objectForPK(existingDataContext,
((DataObject)it.next()).getObjectId());
            }
            catch (Exception e) {
                //we don't care about errors as those will be temp id's in
most cases
            }
        }
    }



    /**
     * play with new contexts and add or remove objects or even fetch
objects and if you want to merge back
     * to another context you need to force the existing context to fetch
the existing objects as it won't
     * touch them in the merge
     *
     * @param existingDataContext
     * @param newDataContext
     */
    public static void syncDataContexts(DataContext existingDataContext,
DataContext newDataContext) {
        Iterator it = newDataContext.getGraphManager
().registeredNodes().iterator();

        while (it.hasNext()) {
            try {
                DataObjectUtils.objectForPK(existingDataContext,
((DataObject)it.next()).getObjectId());
            }
            catch (Exception e) {
                //we don't care about errors as those will be temp id's in
most cases
            }
        }
    }

It all seems very convoluted but it works. I am not sure what side effects
this causes and I am not happy to be manipulating objects at this level as
all might change when you move to 3.0 and everything here breaks.

I would appreciate any thoughts you may have.


Regards

ed


On 11/01/07, Andrus Adamchik <an...@objectstyle.org> wrote:
>
> Hi Edward,
>
> Sorry for delayed reply.
>
> >> AnotherClass newObject =
> >> (AnotherClass)childContext.createAndRegisterNewObject
> >> (AnotherClass.class);
> >> AClass localParentObject = (AClass)childContext.localObject
> >> (parent.getObjectId(),null);
> >>
> >> object.setParent(localParent);
>
>
> I assume 'object' is same as 'newObject'?
>
> >> For some reason I seem to get the parent having two children, not
> >> just the
> >> one! I assume it is because the changes in the childContext are
> >> propagated
> >> to the parent context so as a result the first setParent call adds
> >> the child
> >> to the parent and on commit changes the child is added again to
> >> the parent.
>
> No - actually the changes are only propagated on commit. So this
> behavior does look like a bug. I suggest to check whether this
> happens with the latest code (unofficial 2.0.2 and 1.2.2 builds can
> be downloaded from here: http://people.apache.org/~aadamchik/release/
> 2.0.2/ ) If it still happens, please open a bug report.
>
>
> >  What is the real purpose of passing a prototype?
>
> To merge data from a different object. I must admit that cramming all
> possible merge scenarios in a single 'localObject' method does create
> some confusion. We may come up with a better way to transfer and
> merge objects in the future.
>
> Andrus
>
>
>
> On Jan 8, 2007, at 11:48 AM, edward pedersson wrote:
> > Have been digging around the dataContext and I saw this in the
> > localObject
> > method
> >
> >            // TODO: Andrus, 1/24/2006 implement smart merge for
> > modified
> > objects...
> >            if (cachedObject != prototype
> >                    && state != PersistenceState.MODIFIED
> >                    && state != PersistenceState.DELETED) {
> >
> > I believe this may be causing one of the problems below. If you call
> > localObject with a new object and a prototype then the checks above
> > are true
> > and as a result this gets called further down the chain in the same
> > method
> >
> > descriptor.shallowMerge(prototype, cachedObject);
> >
> > which sets all the relationships to faults and this breaks because
> > the new
> > object is not committed.
> >
> > Is this a bug? What is the real purpose of passing a prototype?
> >
> > On 05/01/07, edward pedersson <cp...@googlemail.com> wrote:
> >>
> >> Hi
> >>
> >> I am using the latest version of Cayenne with Tapestry 4.0.1.
> >>
> >> I am having a bit of trouble using nested dataContexts. I would
> >> like the
> >> user to be able to create a new object in a wizrd style
> >> application but have
> >> the option to cancel half way through the stages.
> >>
> >> I have tried a few different options and noe seem to work. In the
> >> first
> >> instance I tried the nested datacontext like this:
> >>
> >> AClass parent; //assume this exists and is in the threaded context
> >>
> >> DataContext childContext = this.getDataContext().createChildContext
> >> () //
> >> this.getDataContext() is the threaded context
> >>
> >> AnotherClass newObject =
> >> (AnotherClass)childContext.createAndRegisterNewObject
> >> (AnotherClass.class);
> >> AClass localParentObject = (AClass)childContext.localObject(
> >> parent.getObjectId(),null);
> >>
> >> object.setParent(localParent);
> >>
> >> //do other stuff here like set other properties
> >>
> >> // now save to parent
> >>
> >> childContext.commitChangesToParent();
> >>
> >> For some reason I seem to get the parent having two children, not
> >> just the
> >> one! I assume it is because the changes in the childContext are
> >> propagated
> >> to the parent context so as a result the first setParent call adds
> >> the child
> >> to the parent and on commit changes the child is added again to
> >> the parent.
> >>
> >> So I tried to have separate dataContexts instead by doing this
> >>
> >> DataContext anotherContext = DataContext.createDataContext()
> >>
> >> AnotherClass newObject =
> >> (AnotherClass)anotherContext.createAndRegisterNewObject
> >> (AnotherClass.class
> >> );
> >> AClass localParentObject = (AClass)anotherContext.localObject(
> >> parent.getObjectId(),null);
> >>
> >> object.setParent(localParent);
> >>
> >> //do other stuff here like set other properties
> >>
> >> // now save to parent
> >> newObject = (AClass)this.getDataContext().localObject(
> >> newObject.getObjectId(),null);
> >>
> >>
> >> I get errors like Can't build a query for relationship '...' for
> >> temporary
> >> id:
> >>
> >> Does anyone knew what I am doing wrong?
> >>
> >> many thanks for your help.
> >>
> >> --
> >>
> >>
> >> -- e
> >
> >
> >
> >
> > --
> >
> >
> > -- e
>
>


-- 


-- e

Re: nested dataContexts

Posted by Andrus Adamchik <an...@objectstyle.org>.
Hi Edward,

Sorry for delayed reply.

>> AnotherClass newObject =
>> (AnotherClass)childContext.createAndRegisterNewObject 
>> (AnotherClass.class);
>> AClass localParentObject = (AClass)childContext.localObject 
>> (parent.getObjectId(),null);
>>
>> object.setParent(localParent);


I assume 'object' is same as 'newObject'?

>> For some reason I seem to get the parent having two children, not  
>> just the
>> one! I assume it is because the changes in the childContext are  
>> propagated
>> to the parent context so as a result the first setParent call adds  
>> the child
>> to the parent and on commit changes the child is added again to  
>> the parent.

No - actually the changes are only propagated on commit. So this  
behavior does look like a bug. I suggest to check whether this  
happens with the latest code (unofficial 2.0.2 and 1.2.2 builds can  
be downloaded from here: http://people.apache.org/~aadamchik/release/ 
2.0.2/ ) If it still happens, please open a bug report.


>  What is the real purpose of passing a prototype?

To merge data from a different object. I must admit that cramming all  
possible merge scenarios in a single 'localObject' method does create  
some confusion. We may come up with a better way to transfer and  
merge objects in the future.

Andrus



On Jan 8, 2007, at 11:48 AM, edward pedersson wrote:
> Have been digging around the dataContext and I saw this in the  
> localObject
> method
>
>            // TODO: Andrus, 1/24/2006 implement smart merge for  
> modified
> objects...
>            if (cachedObject != prototype
>                    && state != PersistenceState.MODIFIED
>                    && state != PersistenceState.DELETED) {
>
> I believe this may be causing one of the problems below. If you call
> localObject with a new object and a prototype then the checks above  
> are true
> and as a result this gets called further down the chain in the same  
> method
>
> descriptor.shallowMerge(prototype, cachedObject);
>
> which sets all the relationships to faults and this breaks because  
> the new
> object is not committed.
>
> Is this a bug? What is the real purpose of passing a prototype?
>
> On 05/01/07, edward pedersson <cp...@googlemail.com> wrote:
>>
>> Hi
>>
>> I am using the latest version of Cayenne with Tapestry 4.0.1.
>>
>> I am having a bit of trouble using nested dataContexts. I would  
>> like the
>> user to be able to create a new object in a wizrd style  
>> application but have
>> the option to cancel half way through the stages.
>>
>> I have tried a few different options and noe seem to work. In the  
>> first
>> instance I tried the nested datacontext like this:
>>
>> AClass parent; //assume this exists and is in the threaded context
>>
>> DataContext childContext = this.getDataContext().createChildContext 
>> () //
>> this.getDataContext() is the threaded context
>>
>> AnotherClass newObject =
>> (AnotherClass)childContext.createAndRegisterNewObject 
>> (AnotherClass.class);
>> AClass localParentObject = (AClass)childContext.localObject(
>> parent.getObjectId(),null);
>>
>> object.setParent(localParent);
>>
>> //do other stuff here like set other properties
>>
>> // now save to parent
>>
>> childContext.commitChangesToParent();
>>
>> For some reason I seem to get the parent having two children, not  
>> just the
>> one! I assume it is because the changes in the childContext are  
>> propagated
>> to the parent context so as a result the first setParent call adds  
>> the child
>> to the parent and on commit changes the child is added again to  
>> the parent.
>>
>> So I tried to have separate dataContexts instead by doing this
>>
>> DataContext anotherContext = DataContext.createDataContext()
>>
>> AnotherClass newObject =
>> (AnotherClass)anotherContext.createAndRegisterNewObject 
>> (AnotherClass.class
>> );
>> AClass localParentObject = (AClass)anotherContext.localObject(
>> parent.getObjectId(),null);
>>
>> object.setParent(localParent);
>>
>> //do other stuff here like set other properties
>>
>> // now save to parent
>> newObject = (AClass)this.getDataContext().localObject(
>> newObject.getObjectId(),null);
>>
>>
>> I get errors like Can't build a query for relationship '...' for  
>> temporary
>> id:
>>
>> Does anyone knew what I am doing wrong?
>>
>> many thanks for your help.
>>
>> --
>>
>>
>> -- e
>
>
>
>
> -- 
>
>
> -- e


Re: nested dataContexts

Posted by edward pedersson <cp...@googlemail.com>.
Have been digging around the dataContext and I saw this in the localObject
method

            // TODO: Andrus, 1/24/2006 implement smart merge for modified
objects...
            if (cachedObject != prototype
                    && state != PersistenceState.MODIFIED
                    && state != PersistenceState.DELETED) {

I believe this may be causing one of the problems below. If you call
localObject with a new object and a prototype then the checks above are true
and as a result this gets called further down the chain in the same method

descriptor.shallowMerge(prototype, cachedObject);

which sets all the relationships to faults and this breaks because the new
object is not committed.

Is this a bug? What is the real purpose of passing a prototype?

On 05/01/07, edward pedersson <cp...@googlemail.com> wrote:
>
> Hi
>
> I am using the latest version of Cayenne with Tapestry 4.0.1.
>
> I am having a bit of trouble using nested dataContexts. I would like the
> user to be able to create a new object in a wizrd style application but have
> the option to cancel half way through the stages.
>
> I have tried a few different options and noe seem to work. In the first
> instance I tried the nested datacontext like this:
>
> AClass parent; //assume this exists and is in the threaded context
>
> DataContext childContext = this.getDataContext().createChildContext() //
> this.getDataContext() is the threaded context
>
> AnotherClass newObject =
> (AnotherClass)childContext.createAndRegisterNewObject(AnotherClass.class);
> AClass localParentObject = (AClass)childContext.localObject(
> parent.getObjectId(),null);
>
> object.setParent(localParent);
>
> //do other stuff here like set other properties
>
> // now save to parent
>
> childContext.commitChangesToParent();
>
> For some reason I seem to get the parent having two children, not just the
> one! I assume it is because the changes in the childContext are propagated
> to the parent context so as a result the first setParent call adds the child
> to the parent and on commit changes the child is added again to the parent.
>
> So I tried to have separate dataContexts instead by doing this
>
> DataContext anotherContext = DataContext.createDataContext()
>
> AnotherClass newObject =
> (AnotherClass)anotherContext.createAndRegisterNewObject(AnotherClass.class
> );
> AClass localParentObject = (AClass)anotherContext.localObject(
> parent.getObjectId(),null);
>
> object.setParent(localParent);
>
> //do other stuff here like set other properties
>
> // now save to parent
> newObject = (AClass)this.getDataContext().localObject(
> newObject.getObjectId(),null);
>
>
> I get errors like Can't build a query for relationship '...' for temporary
> id:
>
> Does anyone knew what I am doing wrong?
>
> many thanks for your help.
>
> --
>
>
> -- e




-- 


-- e