May 30, 2011

GraniteDS/Tide Entity Management “Painful Gotchas”

GraniteDS and Data Management

    If you read up on GraniteDS (www.graniteds.org) you'll see it has a pretty slick data management scheme.  GraniteDS/Tide provides a client-side entity cache for each managed entity in the Tide context, which greatly simplifies data management.  What that means is that for every entity that Tide knows about, it will keep a single actionscript representation of that entity through all entity transactions for the life of the context.    This is  a very logical paradigm to work from.  There is only 1 instance of MyObject with id=0 in the database, and with Tide there will only ever be 1 instance MyObject with id=0 in memory of your Flex application.

    This is great news for developers who want to attach event listeners to the entities, and update their visualizations accordingly.  For example, if I have a chart whose data provider is a list of entities, that chart will be updated automatically if anything ever modifies that actionscript entity or we get updates from the database.  There is no need to write custom data managers for your application.  It’s all handled for you.  There are, however, some rules you need to live by if you want to avoid some painfully difficult problems to debug.

Always assign a UID to new entities created on the Flex side

    Always, always, always assign a UID to entities you create on the flex side.  If Tide comes across an entity that it doesn’t recognize because it doesn’t have a UID, it will create a new instance of the entity with a UID and store it in the context.  That means if a new “myEntity” entity is added to a hierarchy of entities, and that entity hierarchy is persisted, Tide will end up replacing your instance of the “myEntity” entity with a new instance of “myEntity”.  And any references or listeners attached to the original “myEntity” are useless, and will never be updated.

Always use PersistentCollection for collection attributes created on the Flex side

    This is probably a “duh” if you ever looked at your entities in the Flex debugger.  Granite/Tide will replace your standard flex list collections with “PersistentCollection” after data is persisted.  So if your actionscript entity had a list collection attribute and you initialized it with an “ArrayCollection”, the data will be persisted just fine, but any listeners you attached to the collection will be useless after Granite/Tide swaps out the “ArrayCollection” with an instance of a “PersistentCollection”.
    However, while the loss of collection listeners might be obvious because of the swapping of collection instances, there exists another much subtler problem.  Granite/Tide won’t always pick up and add child entities to the Context if you aren’t using a “PersistentCollection“.  Consider an entity hierarchy E1->E2->E3, where “->” represents a collection attribute that contains another entity.  If the attribute represented by the “->” are “ArrayCollection” rather than “PersistentCollection”, Tide/Granite will swap out the E3 object, even if the UID attribute is set.  The reason is E3 wasn’t added to the Context and Granite/Tide won’t be able to find a managed entity to update, and thus will create a new instance.

What happens if you don’t follow those rules?

    In the cases where you don't follow the rules, you can expect some very difficult bugs.  Namely, your application won’t work as expected because your property change event listeners won’t fire.  This is a very strange phenomenon to guard against because unless you are writing unit tests that run on an application server with a full GraniteDS/database stack, your unit tests will show that your application works just fine.  This was recently the case for me.  The UI of my application worked fine, unit tests passed, etc.  However, things just stopped working after I started persisting data.  No errors, no interesting log events, no anything.  My logic was operating properly, e.g. a successful drag and drop added the dropped item to be the child of the parent “Folder” entity.  But the UI didn’t reflect the new parent/child relationship.  What the heck?!
    What I later discovered, through some long and painful debugging, was that I was not listening to the CORRECT entities anymore.   In my code I would:

  1. create a new “Folder” entity,
  2. setup a listener on the “folder.children” collection attribute,
  3. then much later add a child to “folder.children”

but the collection listener wasn’t being fired?
    The root cause was that I wasn’t following the rules above.  I wasn’t setting the UID attribute on my entities, so Tide was replacing the entities and collections I was listening to with new instances after my first successful persist.   And I wasn’t using the correct collection class, “PersistentCollection”, and so nested entities weren’t added to the context and were ultimately replaced after a persist.  Ultimately this meant my fully functioning UI code was listening to objects that were never going to be modified again, and the only way to detect this issue is to look at the memory address of each object in the debugger and see if it is still the same memory address at the time you created the object.


Debugging Mind Freak:  What makes this bug even worse is if you are running your code in a debugger and display the ”uid” attribute, the Flex framework will automatically set the UID for you, thus causing your code to potentially work correctly when stepping through your code.  Makes your head hurt doesn’t it?!

The Example

    Take a look at the simple unit test  below.  The test creates a Folder object that has a "children" collection attribute, adds another “Folder” as a child to the first Folder, adds an REProperty object as a child to the second Folder, and finally persists the entity hierarchy.  Also note that “folderToWatch” is injected into the Tide context which means Tide will attempt to manage all the entities in the entity hierarchy.  Take a look at what happens if I don’t set the “uid” attribute or use “ArrayCollection”  instead of “PersistentCollection” on the entities.

 
package org.granite.granitetests
{
  import com.jnyinvestments.mdiframework.entity.basicdata.Folder;
  import com.jnyinvestments.reiapp.entity.reidata.REProperty;
  import com.jnyinvestments.reiapp.session.entityhomes.BasicPersistanceTestBase;
 
  import flash.events.TimerEvent;
 
  import flexunit.framework.Assert;
 
  import mx.collections.ArrayCollection;
  import mx.collections.ListCollectionView;
  import mx.events.CollectionEvent;
  import mx.utils.UIDUtil;
 
  import org.granite.persistence.PersistentBag;
  import org.granite.tide.Component;
  import org.granite.tide.collections.PersistentCollection;
  import org.granite.tide.events.TideFaultEvent;
  import org.granite.tide.events.TideResultEvent;
 
  [Bindable]
  [Name("ContextAndPersistanceTests")]
  public class ContextAndPersistanceTests extends BasicPersistanceTestBase
  {  
    /**
     * Home component for persisting "Folder" objects.
     */

    [In(create="true")]
    public var folderHome:Component;
   
    /**
     *  Folder instance that will be created, and watched by
     *  tide because we are adding it to the context.
     */

    [In(create="false")][Out]
    public var folderToWatch:Folder;
   
    /**
     * Folder that will be created and added to the
     * "folderToWatch" folder as a child.
     */

    private var anotherFolderToWatch:Folder;
   
    /**
     * Child property that will be added as a child to the Folder.
     * Not managed by tide.
     */

    private var propertyToWatch:REProperty;
   
    /**
     * List collection that will refer to the "children" attribute of the Folder
     * entity.
     */

    private var _childCollectionToWatch:ListCollectionView;
   
    /**
     * Sets up the test environment.  Registers this test with
     * the Tide context.
     */

    [Before (async)]
    public override function setUp():void
    {
      super.setUp();
   
//      Seam.getInstance().initApplication();
//      globalContext.testApp = this;
     
      folderHome.id = null;
      folderHome.instance = null;
    }
   
   
    /**
     * This test case will
     *  1.  Construct a Folder entity
     *  2.  Construct a REProperty entity
     *  3.  Adds REProperty as child to Folder
     *  4.  Persists entity hierarchy
     *  5.  Test whether any of the references are no longer valid
     *  6.  Changes data & persist entity hierarchy again
     *  7.  Test whether any of the references are no longer valid
     */

    [Test(async)]
    public function watchFolderChildrenBetweenMerges():void
    {
      folderToWatch = new Folder();
      //folderToWatch.uid = UIDUtil.createUID();
      folderToWatch.objectName = "Test Folder";
      folderToWatch.children = new PersistentCollection(folderToWatch, "children", new PersistentBag());
     
      anotherFolderToWatch = new Folder();
      //anotherFolderToWatch.uid = UIDUtil.createUID();
      anotherFolderToWatch.objectName = "Another Folder To Watch";
      anotherFolderToWatch.children = new ArrayCollection();//new PersistentCollection(anotherFolderToWatch, "children", new PersistentBag());
     
     
      propertyToWatch = new REProperty();
      //propertyToWatch.uid = UIDUtil.createUID();
      propertyToWatch.objectName = "Test Property";
     
      _childCollectionToWatch = anotherFolderToWatch.children;
     
      folderToWatch.children.addItem(anotherFolderToWatch);
      anotherFolderToWatch.children.addItem(propertyToWatch);
     
      Assert.assertTrue(isNaN(folderToWatch.id));
      Assert.assertTrue(isNaN(propertyToWatch.id));
      Assert.assertTrue(isNaN(anotherFolderToWatch.id));
      Assert.assertEquals(anotherFolderToWatch, folderToWatch.children.getItemAt(0));
      Assert.assertEquals(propertyToWatch, anotherFolderToWatch.children.getItemAt(0));
      Assert.assertEquals(anotherFolderToWatch.children, _childCollectionToWatch);
     
      //Persist the entity hierarchy
      merge(folderHome, folderToWatch, verifyFolderChildrenAfterMerge);
    }
   
    private function verifyFolderChildrenAfterMerge(event:TideResultEvent, passThroughData:Object = null):void
    {
      //This will succeed even if UID is not set.  The reason is because
      //the folder received a UID automatically when it was attached
      //to the "folderHome" and was registered with Tide via injection.
      Assert.assertFalse(isNaN(folderToWatch.id));
     
      //These will succeed.
      Assert.assertFalse(isNaN(anotherFolderToWatch.id));    
      Assert.assertEquals(anotherFolderToWatch, folderToWatch.children.getItemAt(0));
     
      //This will fail because ArrayCollection was swapped out with an instance
      //of PersistentCollection.
      Assert.assertEquals(anotherFolderToWatch.children, _childCollectionToWatch);
     
      //These will fail if UID is not set on the propertyToWatch AND
      //anotherFolderToWatch.children was originally a PersistentCollection
      Assert.assertEquals(propertyToWatch, anotherFolderToWatch.children.getItemAt(0));
      Assert.assertFalse(isNaN(propertyToWatch.id));
     
      //Make some more changes to see if things are swapped out again after
      //a second merge.
      folderToWatch.objectName = "changed folder";
      _childCollectionToWatch = anotherFolderToWatch.children;
      propertyToWatch = anotherFolderToWatch.children.getItemAt(0) as REProperty;
      propertyToWatch.objectName = "changed property";
     
      //Persist the entity hierarchy
      merge(folderHome, folderToWatch, verifyFolderChildrenAfterSecondMerge);
    }
   
    private function verifyFolderChildrenAfterSecondMerge(event:TideResultEvent, passThroughData:Object = null):void
    {
      //These all succeed and nothing has been swapped out.
      Assert.assertFalse(isNaN(folderToWatch.id));
      Assert.assertFalse(isNaN(anotherFolderToWatch.id));
      Assert.assertEquals(anotherFolderToWatch, folderToWatch.children.getItemAt(0));
      Assert.assertFalse(isNaN(propertyToWatch.id));
      Assert.assertEquals(anotherFolderToWatch.children, _childCollectionToWatch);
      Assert.assertEquals(propertyToWatch, anotherFolderToWatch.children.getItemAt(0));
    }
   
  }
}
Parsed in 0.063 seconds at 93.03 KB/s
There are a couple interesting points worth calling out in this test application:
  1. The member variable “folderToWatch” was added to the tide context prior to persisting the data.
    • Because “folderToWatch” was injected, Tide will manage this entity.
  2. The member variable “anotherFolderToWatch” was added to the tide context prior to persisting the data.
    • Because Tide was able to successfully scan the “PersistentCollection”, the child entity was added to the context.  I.e. if you look at the “GlobalContext._entityManager._entitiesByUID” you will see entries for both “folderToWatch” and “anotherFolderToWatch”.  This means this entity instance will remain valid and receive data updates.
  3. The member variable “propertyToWatch” was NOT added to the tide context prior to persisting the data.
    • This is because Granite/Tide was not able to scan the “ArrayCollection” children attribute on the “anotherFolderToWatch” entity.
  4. The member variable "anotherFolderToWatch" still equals (==) "folderToWatch.children.getItemAt(0)" after the persist.
    • Even though this object did not have UID, Tide did NOT swap out the folder after merging it.  So any event listeners you had attached to an injected variable will work as expected.  The reason is Granite added the object to a “UIDWeakSet” which accessed the “uid” attribute, which caused adobe Flex to automatically set the “uid” value for you.  And since it was in the context prior to the merge, it will be updated by tide after the merge.
  5. “propertyToWatch” no longer equals (==) “anotherFolderToWatch.children.getItemAt(0)”.
    • “propertyToWatch” was swapped out by Granite/Tide for two reasons.  First, because the parent collection was an “ArrayCollection” Tide/Granite didn’t scan it to look for entities to add to the context.  Second, the  “propertyToWatch”didn’t have a UID, and there was nothing that called “uid” to set it automatically.  If either of those

 

Fixing the Example

  1. Setting “anotherFolderToWatch.children = new PersistentCollection(anotherFolderToWatch, "children", new PersistentBag())”
  2. Uncomment out the setting of uids, e.g. “propertyToWatch.uid = UIDUtil.createUID();”

Fixing those two issues will ensure that entities are managed properly and that entities or collections aren’t inadvertantly swapped out from under you.

 

Summary

    Granite/Tide entity management is fantastic provided you use it properly.  When you don’t, however,  it can be a real headache.


Questions?  Post a comment, or contact me at plummeronsoftware at gmail dot com

1 comment:

Anonymous said...

[url=http://aluejxfttk.com]LXNhsIzMQXovNsee[/url] , yrEhMhFxfZtge - http://pyfnknfrtw.com