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

13 comments:

Hua Cai said...

ray-ban sunglasses
louis vuitton bags
michael kors outlet online
lululemon uk
louis vuitton outlet
swarovski outlet
louis vuitton outlet stores
michael kors factory outlet
burberry outlet
mulberry outlet
hermes outlet store
ray ban sunglasses
michael kors handbags
cheap oakley sunglasses
hermes outlet
air max 2015
fitflops outlet sale
louis vuitton handbags
ferragamo outlet
tiffany and co
nike tn pas cher
rolex uk
mulberry sale
replica watches
bottega veneta outlet online
cheap michael kors handbags
fitflops clearance
toms shoes
christian louboutin outlet
chaussure louboutin
celine outlet online
toms outlet
cheap jordan shoes
nike blazer pas cher
ferragamo outlet
20160528caihuali

xumeiqing said...

20160620meiqing
sac longchamp pliage
coach factory outlet
coach outlet
coach factory outlet
adidas nmd runner
converse shoes
burberry outlet
fitflops sale clearance
nike roshe run women
fitflops
gucci handbags
burberry outlet
ray ban outlet
cheap oakley sunglasses
ray ban sunglasses
louis vuitton outlet
burberry outlet
asics outlet
yeezy boost 350
reebok uk
ray bans
hermes belt
kate spade outlet
canada goose
holliste sale
cartier watches
yeezy boost 350 black
michael kors outlet
nike trainers
chaussure louboutin
kate spade outlet
christian louboutin shoes
louis vuitton factory outlet
nike free flyknit 4.0
bottega veneta handbags
running shoes
reebok pump

xjd7410@gmail.com said...

discount jordans
air jordans
coach factory outlet
retro 11
louboutin shoes
ray ban sunglasses
kobe 8
rolex watches
giuseppe zanotti
oakley sunglasses
adidas running shoes
tiffany jewelry
adidas superstar trainers
gucci handbags
louis vuitton purses
lebron 12
louis vuitton outlet
burberry handbags
air jordan femme
timberland outlet
michael kors outlet online
michael kors outlet
nike store
coach outlet online
ray ban sunglasses
fit flops
coach factory outlet online
louis vuitton handbags
supra for sale
coach outlet online
chenyingying712

dong dong23 said...

san antonio spurs jerseys
christian louboutin sale
air jordan pas cher
burberry outlet
moncler pas cher
ugg sale
louis vuitton
michael kors outlet
sac longchamp
jordan shoes
201610.6wengdongdong

raybanoutlet001 said...

basketball shoes
under armour outlet
nike store
nike outlet
michael kors handbags online
birkenstock sandals
true religion jeans
michael kors handbags
fitflops sale clearance
michael kors handbags sale

raybanoutlet001 said...

nike blazer
basketball shoes
ecco shoes outlet
yeezy boost 350 black
michael kors outlet online
chicago bulls jersey
michael kors handbags wholesale
adidas nmd runner
nhl jerseys
tiffany and co

dada24 Xu said...

louis vuitton pas cher
hermes bags
pandora charms sale clearance
cheap jordans for sale
longchamp handbags
uggs
moncler outlet online
canada goose
adidas nmd r1
kd shoes
zhi20161209

dong dong23 said...

gucci outlet
louis vuitton handbags
prada outlet
pandora uk
ugg outlet
timberland boots
ralph lauren outlet
celine outlet
nike air max pas cher
ralph lauren
201612.27wengdongdong

Meiqing Xu said...

birkenstocks
levis outlet online
louis vuitton handbags
coach factory outlet
true religion
fitflops
red bottoms shoes
christian louboutin shoes
fake rolex
toms outlet
20170209CAIYAN

LCc 03 said...

longchamps
adidas stan smith shoes
hogan outlet online
yeezy shoes
harden shoes
yeezy shoes
links of london
hermes belt
nike roshe run
kobe basketball shoes

John said...

longchamp handbags
gucci borse
mbt
adidas
red bottom
adidas yeezy boost
yeezy boost 350 v2
cheap nike sneakers
oakley vault
ralph lauren sale clearance uk
20170707yuanyuan

adidas nmd said...

ugg outlet
true religion outlet store
oakley sunglasses
kobe 9
michael kors handbags
mont blanc outlet
ugg outlet
nike blazer pas cher
ugg boots
michael kors handbags

aaa kitty20101122 said...

nmd
kobe shoes
michael kors handbags
nmd
adidas tubular
nike air zoom
fitflops
adidas ultra
prada sunglasses
longchamp bags