These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 4 posts ] 
Author Message
 Post subject: Entity with Map causes problems with versioning?
PostPosted: Wed Aug 19, 2009 1:07 am 
Newbie

Joined: Mon Sep 08, 2003 5:47 pm
Posts: 9
Location: Denver
I have an entity with a property that is a Map<String, String> and uses an version # for optimistic locking.

When I load an instance of this class using
Code:
session.load(ClassWithMap.class, idValue)
and resave the object with saveOrUpdate() the version # does not change. This is good, because the object HAS NOT changed.

BUT, if I retrieve this same object using a Query or Criteria search and then save it with saveOrUpdate(), the version # is incremented. This appears to be from CollectionType.isEqual(Object, Object, EntityMode) using the '==' operator to see that the instance's collection is not just equal but actually the same collection instance. I traced through the following (Hibernate 3.2.7.ga):
Code:
TypeFactory.findDirty[line 599]
   properties[i].getType().isDirty( previousState[i], currentState[i], includeColumns[i], session )

CollectionType[line 291].isDirty(…)
CollectionType[line 284].isDirty(…)
   return isOwnerVersioned( session ) is true && super.isDirty( old, current, session );

AbstractType.isDirty[line 70]
   return !isSame( old, current, session.getEntityMode() );

AbstractType.isSame[line 103]
      return isEqual(x, y, entityMode);

CollectionType.isEqual[line 90]
   return x == y
         || ( x instanceof PersistentCollection && ( (PersistentCollection) x ).isWrapper( y ) )
         || ( y instanceof PersistentCollection && ( (PersistentCollection) y ).isWrapper( x ) );



Here's the class mapping - note the 'lazy="false"' just in case that's important:
Code:
   <class name="eg.ClassWithMap" table="test_table" lazy="false">
      <id name="objectId" column="object_id">
         <generator class="sequence">
            <param name="sequence">test_id_sequence</param>
         </generator>
      </id>

      <version name="optimisticVersion" column="instance_version" unsaved-value="negative" />

      <property name="identifierName" column="identifier_name" />
      <property name="descriptiveName" column="descriptive_name" />

      <map name="parameterValues" table="map_parms"
         lazy="false" cascade="all" optimistic-lock="false">
         <key column="object_id" />
         <map-key column="p_name" type="string" />
         <element column="p_value" type="string" />
      </map>
   </class>


And here's a JUnit that demonstrates a couple of oddities:
Code:
   public void testHibernateQueryIncrementsVersionProblem() throws Exception
   {
      Map<String, String> parms = new HashMap<String, String>();
      ClassWithMap lc = new ClassWithMap("lc_test","LC Test",parms);
      
      Session session = SessionFactoryUtils.getNewSession(sessionFactory);
      session.beginTransaction();
      Serializable lcId = session.save(lc);
      logger.info("saved ClassWithMap to id: " + lcId);
      session.getTransaction().commit();
      assertEquals("version", 0, lc.optimisticVersion);

      for (int index = 0; index < 20; index ++) {
         session = SessionFactoryUtils.getNewSession(sessionFactory);
         session.beginTransaction();
         Query query = session.createQuery("from ClassWithMap as theObject where theObject.objectId = :objId");
         query.setParameter("objId", lc.objectId);
         List<ClassWithMap> persistentVersions = query.list();
         session.getTransaction().commit();
         SessionFactoryUtils.releaseSession(session, sessionFactory);
                  
         assertEquals("there's only 1", 1, persistentVersions.size());
      
         ClassWithMap queriedVersion = (ClassWithMap) persistentVersions.get(0);
         assertEquals("The just queried version", i, queriedVersion.optimisticVersion);

         session = SessionFactoryUtils.getNewSession(sessionFactory);
         session.beginTransaction();
         session.saveOrUpdate(queriedVersion);
         session.getTransaction().commit();
         SessionFactoryUtils.releaseSession(session, sessionFactory);

         assertEquals("After saving, it has mistakenly incremented version #", index +1, queriedVersion.optimisticVersion);
      }
      
      session = SessionFactoryUtils.getNewSession(sessionFactory);
      session.beginTransaction();
      ClassWithMap rehy2 = (ClassWithMap)session.load(ClassWithMap.class, lc.objectId);
      session.getTransaction().commit();
      SessionFactoryUtils.releaseSession(session, sessionFactory);

      // this should fail - but it doesn't
      assertEquals("now the version's got nothing to do with the value stored in the db - should be 20", 0, rehy2.optimisticVersion);
      
      session = SessionFactoryUtils.getNewSession(sessionFactory);
      session.beginTransaction();
      session.saveOrUpdate(rehy2);
      session.getTransaction().commit();
      SessionFactoryUtils.releaseSession(session, sessionFactory);

      // this should fail too
      assertEquals("it still has the wrong version after saving", 0, rehy2.optimisticVersion);

   }


What am I overlooking? What am I missing?

Thanks,
-- Chris


Top
 Profile  
 
 Post subject: Re: Entity with Map causes problems with versioning?
PostPosted: Thu Aug 20, 2009 10:49 am 
Newbie

Joined: Mon Sep 08, 2003 5:47 pm
Posts: 9
Location: Denver
It looks like when loading from a query, there's a point where the collection is looked up using a CollectionKey in a LoadContexts instance:
Code:
   LoadingCollectionEntry locateLoadingCollectionEntry(CollectionKey key) {
      if ( xrefLoadingCollectionEntries == null ) {
         return null;
      }


In my case this xrefLoadingCollectionEntries is null so the return value is null and this bubbles up to CollectionType.getCollection() where the following decides to create a new collection (and thus a different system id which is going to cause my version to increment):
Code:
Collection.getCollection() [line 608]
   ...
         if ( collection == null ) {
            // create a new collection wrapper, to be initialized later
            collection = instantiate( session, persister, key );
            collection.setOwner( owner );


I'm still chasing this down but it's got me worried that it might not be a simple config change. The objects in question are largely reference data and I'm getting all sorts of performance issues with each process in contention to unnecessarily update the version #s.


Top
 Profile  
 
 Post subject: Re: Entity with Map causes problems with versioning?
PostPosted: Thu Aug 20, 2009 10:33 pm 
Newbie

Joined: Mon Sep 08, 2003 5:47 pm
Posts: 9
Location: Denver
It appears that the version # with be incremented for any versioned class where it is included in a saveOrUpdate() in a session different than the one that created it.

The point of interest is in DefaultFlushEntityEventListener's isUpdateNecessary(FlushEntityEvent, boolean) method:
Code:
   private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
      final Status status = event.getEntityEntry().getStatus();
      if ( mightBeDirty || status==Status.DELETED ) {
         // compare to cached state (ignoring collections unless versioned)
         dirtyCheck(event);
         if ( isUpdateNecessary(event) ) {
            return true;
         }
         else {
            FieldInterceptionHelper.clearDirty( event.getEntity() );
            return false;
         }
      }
      else {
         return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status );
      }
   }


The two most relevant steps are the calls to dirtyCheck(event) and isUpdateNecessary(event). In dirtyCheck(event):
  • there's an attempt to let the interceptor findDirty() - but the interceptor is set to EmptyInterceptor which just returns null
  • a cannotDirtyCheck flag is set because there is no loadedState in the event (presumably because it wasn't loaded by this session)
  • next the PersistenceContext (StatefulPersistenceContext) is asked for a cached database snapshot (getCachedDatabaseSnapshot(EntityKey key):300) which consults an internal HashMap called entitySnapshotsByKey which is empty (presumably because this is a new session)
  • so the dirtyCheck() says: "Hey, I can't tell, the interceptor doesn't know, the "loadedState" isn't set and there's no database snapshot available."

Then DefaultFlishEntityEventListener's isUpdateNecessary() says "if there's no dirty check possible just say it needs an update." Finally DefaultFlishEntityEventListener's.onFlushEntity[line 128] calls scheduleUpdate[line 220] which on line 257 updates the version #.

Net result: reattaching a versioned object to a session and calling saveOrUpdate will always cause the version # to increment even if there have been no changes.

It looks like adding selectbefore-update="true" fixes the problem in my simple example project but at the cost of an extra select call. It doesn't appear to have an effect on the real project.

There has to be a better way to do this. Can't I manage my own "isDirty()" method somehow? Certainly my value object can know if it's been modified and avoid both unnecessary snapshot loads and unnecessary and contentious version updates.


Top
 Profile  
 
 Post subject: Re: Entity with Map causes problems with versioning?
PostPosted: Fri Aug 21, 2009 5:05 pm 
Newbie

Joined: Mon Sep 08, 2003 5:47 pm
Posts: 9
Location: Denver
After a little thought, I'm going to go with a design change to the app rather than build a custom isDirty() mechanism. If it was just a matter of catching when the local object changed, fine, but there's the problem of making sure I'm not stepping on other processes having updated the persistent state too.

So I'm removing any automagic cascading saves that cover these very often read and very little changed objects and requiring that my code explicitly chose when to save them. That way I can better cope with a concurrent update failure and remove any need for the update/selects required to version these objects.

Note to world: this is only really an issue because I have to deal with these objects in separate sessions. Why? Because they're part of a grid and often are not being saved on the same jvm or physical machine that they were created upon and I don't want to contemplate the complexity of a grid-wide session. (Que Ari from Terracotta, just how easy is it Ari?)

Anyhow, been nice chatting with you all ;-) time to get back to work.

- C


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 4 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.