Issue Details (XML | Word | Printable)

Key: GDS-103
Type: Bug Bug
Status: Resolved Resolved
Resolution: Fixed
Priority: Major Major
Assignee: Franck Wolff
Reporter: Viral B
Votes: 0
Watchers: 3
Operations

If you were logged in you would be able to see more operations.
GraniteDS

Saving deserialized Hibernate entity with uninitialized collection throws exception "could not reassociate uninitialized transient collection"

Created: 17/Apr/08 02:41 AM   Updated: 24/Feb/09 03:09 PM   Resolved: 19/Feb/09 04:36 PM
Component/s: AMF3 (de)serialization, AS3 code generation
Affects Version/s: 1.1.0_RC1
Fix Version/s: 2.0.0_B1

Environment: Flex 2.0.1, Spring, Granite DS 1.1.0, Weblogic 8.1


 Description  « Hide
I am fetching an entity "Person" using Hibernate session, which has
uninitialized collection "addresses".

I am passing person object to flex using HibernateExternalizer.

On Flex UI, I am just updating person attributes and not touching
collection.

When I click on save,
on the server side, I do
Session session = getSession();
session.savOrUpdate(person);

I get "could not reassociate uninitialized transient collection" Hibernate exception.

This is because detached Hibernate uninitialized collection needs to have "role" and "key" attributes.
which are missing in the deserialized HibernatePersistentSet object.

I noticed that while serializing PersistentCollection instance under the hood we also serialize "initialized", "initializing" and "dirty" attributes, but not "role", "owner" and "key" attributes, which raises this exception.

Sort Order: Ascending order - Click to sort in descending order
Franck Wolff added a comment - 23/Apr/08 06:35 PM
use session.merge.

Deepak Surti added a comment - 24/Apr/08 10:56 AM
When sending data back to the database, for uninitialized collections; Granite sends an new empty
instance of the appropriate Hibernate wrapper collection.

Example : instance = new PersistenceSet() ; //when uninitialized.

However on Hibernate side, it fails in ProxyVisitor which checks for the validity of an uninitialized
collection. From proxy visitor in hibernate 3.2.5.ga:

 private static boolean isCollectionSnapshotValid(PersistentCollection snapshot) {
         return snapshot != null &&
                 snapshot.getRole() != null &&
                 snapshot.getKey() != null;
 }

 The instance sent above by granite has role and key to be null. As such one would get the
 "could not reassociate uninitialized transient collection" error.

  A possible fix :
  ------------------

  Why not remember the hibernate AbstractPersistentCollection instance when granite creates the
  HibernatePersistent(X) instance.Then when sending back for uninitialized instances; we send
  back the remembered hibernate AbstractPersistentCollection instance.

  So in HibernateAbstractPersistentCollection I have :

  private AbstractPersistentCollection uninitializedCollection;

  public AbstractPersistentCollection getUninitializedCollection() {
return uninitializedCollection;
}

public void setUninitializedCollection(
AbstractPersistentCollection uninitializedCollection) {
this.uninitializedCollection = uninitializedCollection;
}

  And say in Hibernate PersistentSet I have :

   public HibernatePersistentSet(PersistentSet value) {
        super(Hibernate.isInitialized(value), value.isDirty());
        if (isInitialized())
            content = value.toArray();
       // this is the first part of the fix
        else
         setUninitializedCollection(value);
        
    }

   public Object newInstance() {
        Object instance = null;

        if (isInitialized()) {
            if (content != null) {
                Set<Object> set = new HashSet<Object>(content.length);
                for (Object o : content)
                    set.add(o);
                instance = new PersistentSet(null, set);
                if (isDirty())
                    ((PersistentSet)instance).dirty();
            }
        } else
            // this is second part of the fix
            instance = getUninitializedCollection();

        return instance;
    }
  
   I have tried this out and it works fine.

Gerrie Kimpen added a comment - 27/Jan/09 04:05 PM
We're facing the same problem. Is there a reason why the Granite team did not implement the fix as suggested by Deepak Surti (many tnx) ?

Gerrie Kimpen added a comment - 29/Jan/09 08:25 AM
Comment on Deepak's fix:

The newInstance factory method on HibernatePersistentSet will always return null instead of the original uninitialized collection. Granite maintains no state on the server.

I fixed this issue by sending org.hibernate.collection.AbstractPersistentCollection#getKey() and org.hibernate.collection.AbstractPersistentCollection#getRole() over the wire. At reconstruction time (= in the newInstance method) I do:

    @Override
    public Object newInstance(Type target) {
     PersistentSet instance = null;

     boolean sorted = SortedSet.class.isAssignableFrom(ClassUtil.classOfType(target));
        if (isInitialized()) {
            if (content != null) {
                Set<Object> set = (sorted ? new TreeSet<Object>() : new HashSet<Object>(content.length));
                for (Object o : content)
                    set.add(o);
                instance = (sorted ? new PersistentSortedSet(null, (SortedSet<?>)set) : new PersistentSet(null, set));
                checkDirtiness(instance);
            }
        } else {
            instance = (sorted ? new PersistentSortedSet() : new PersistentSet());
            // <fix>
            instance.setSnapshot(key, role, instance);
            // </fix>
        }

        return instance;
    }

Hope this helps.

Franck Wolff added a comment - 02/Feb/09 12:30 PM
I didn't see these comments earlier... I'll fix that in the next major release (2.0+).

Franck Wolff added a comment - 19/Feb/09 04:36 PM
This is done with an optional configuration option in granite-config.xml, eg:

<granite-config scan="true">
    <externalizers>
        <configuration>
            <hibernatecollectionmetadata>[yes, no, lazy]</hibernatecollectionmetadata>
        </configuration>
    </externalizers>
</granite-config>

'no' is the default.
'lazy' means: serialize key, role, snapshot only for uninitialized collections.
'yes' means: serialize key, role, snapshot always.


Gerrie Kimpen added a comment - 23/Feb/09 12:23 PM
Unfortunately still a problem with saveOrUpdate & CascadeType.DELETE_ORPHAN.

When the Hibernate saveOrUpdate cascades to a one-to-many association, it uses the snapshot to check whether or not it has to delete orphaned entities. In the case that orphans need to be deleted an error is thrown. Entities that are passed to the "delete" method cannot be in a detached state:


Caused by: java.lang.IllegalArgumentException: Removing a detached instance be.kg.persoon.entity.Localisatie#8A87EECD1F5B4A66011F5B4BE2D30001
at org.hibernate.ejb.event.EJB3DeleteEventListener.performDetachedEntityDeletionCheck(EJB3DeleteEventListener.java:45)
at org.hibernate.event.def.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:86)
at org.hibernate.impl.SessionImpl.fireDelete(SessionImpl.java:775)
at org.hibernate.impl.SessionImpl.delete(SessionImpl.java:758)
at org.hibernate.engine.Cascade.deleteOrphans(Cascade.java:355)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:324)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:242)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:219)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
at org.hibernate.engine.Cascade.cascade(Cascade.java:130)
at org.hibernate.engine.Cascade.cascade(Cascade.java:97)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.cascadeOnUpdate(DefaultSaveOrUpdateEventListener.java:357)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:329)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:223)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:89)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:70)
at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:507)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:499)
...

Franck Wolff added a comment - 23/Feb/09 05:35 PM
Hum...

Ok: I (and most people) never use directly a Hibernate session and there is no saveOrUpdate in the standard EntityManager class. If you find a solution,
post a patch, I'll be happy to commit the fix.

Regards, Franck.

Gerrie Kimpen added a comment - 24/Feb/09 09:27 AM
I think we've reached the limits of Granite's support for Hibernate here.

Switching to JPA seems the only senseble way to go ...

Franck Wolff added a comment - 24/Feb/09 11:00 AM
Before any switch, make you have:

1. Updated all GDS jars built from the trunk (checkout or update the graniteds project).
2. Updated Gas3/Granite Eclipse Builder plugins and regenerated all your *Base.as beans. Note that generator classes have moved from the graniteds project to the graniteds_builder project. You should checkout this project, run the prebuild.xml Ant script, export the the plugin (open plugin.xml and use the "Export Wizard" in the first tab). Then, remove both the org.granite.gas3... directory and the org.granite.builder... jar, and add the new org.granite.builder_2.0.0.B1.jar (restart Eclipse). The new jar contains both the Ant task and the builder.
2. Updated all GDS swc built from the trunk and rebuilt your Flex application. Note that there is no specific swc for Hibernate anymore: all AS3 persistence classes are now in the core granite.swc library.

Tell me if it works after those changes.

Franck.

Gerrie Kimpen added a comment - 24/Feb/09 03:09 PM
Thanks for your suggestions Franck.

Yesterday I already took some steps in that direction (to test your patch actually):
1. I've checked out the HEAD revision of the trunk (23 feb, around 9am)
2. I've build the SWCs and JARs using the Ant script. (I indeed noticed that there is no Hibernate swc any more)
3. I've build our app against the new Granite build after making the following changes:
  a. Added <hibernatecollectionmetadata>yes</hibernatecollectionmetadata> to the config file.
  b. Adapted our generic read/writeExternal methods (we do not generate *Base.as beans) because I noticed a change in sending the 'detachedState' over the wire ("0[ProxyDesc]" = for lazy, "1" for initialized).

Result: the collection snapshots travel nicely between server and client (in both directions), but then there is the CascadeType.DELETE_ORPHAN issue.

Let me know if it is still useful to precisely execute the steps you suggested! (Step 2 is quite overwhelming and I don't know for sure if it applicable to our project)

FYI: We actually started of using JPA merge. But merge is a real pain since it constantly returns new instances that we have to replace in the total object graph. Switching to Hibernate saveOrUpdate solved that issue for all our use cases at once, but unfortunately introduced another issue ...