Unique instances

May 18, 2011 at 8:31 PM

It was my impression that Sterling was making sure not to create new instances of previously loaded instances. Let's say I have two tables for types A and B, and that A has a List<C> property, with each C having a property of type B. There is no table for C.

Loading all A's (thus C's and their B), then all B's is creating duplicate instances of B's. Is that expected behavior? Would adding a table for C change something?

Thank you.

May 18, 2011 at 8:43 PM

Tried adding a table for C, doesn't change anything. Was my "unique instances" assumption an error? Can't remember where I got this from.

Coordinator
May 18, 2011 at 9:46 PM

If you defined A and B as a table, and C references B, you should get exactly one copy of B per key as defined. How are you getting more than one B in the table, if the keys are unique? Are you getting duplicate instances with the same key? I'm not sure I follow how it is behaving unexpectedly. If you have a key of 2 for a B table, then you have an A with C that points to B with a key of 2, you should get exactly one B with a key of 2.

May 18, 2011 at 10:31 PM

That's not what I see. I'll try to create a test case. My keys are guids, so it's harder to follow. :)

May 20, 2011 at 3:44 AM

Finally back on the net, dead modem for the last 24 hours. :(

I reproduced the problem in a closed test case. It fails with both the default memory driver and IsolatedStorageDriver. First, the code:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using Microsoft.Silverlight.Testing;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wintellect.Sterling;
using Wintellect.Sterling.Database;
using Wintellect.Sterling.Keys;
using Wintellect.Sterling.IsolatedStorage;

namespace DutchTab.Tests.Confined
{
  public abstract class Base
  {
    public Guid Id { get; set; }
  }

  public class Bill : Base
  {
    public Bill()
    {
      this.Partakers = new List<Partaker>();
    }

    public string Name { get; set; }
    public List<Partaker> Partakers { get; set; }
    public double Total { get; set; }
  }

  public class Person : Base
  {
    public string Name { get; set; }
  }

  public class Partaker : Base
  {
    public double Paid { get; set; }
    public Person Person { get; set; }
  }

  public class SameInstancesDatabase : BaseDatabaseInstance
  {
    public override string Name
    {
      get { return "SameInstancesDatabase"; }
    }

    protected override List<ITableDefinition> RegisterTables()
    {
      return new List<ITableDefinition>
        {
            CreateTableDefinition<Bill, Guid>( b => b.Id ),
            CreateTableDefinition<Person, Guid>( p => p.Id )
        };
    }
  }

  [Tag( "confined" )]
  [TestClass]
  public class SameInstancesTests
  {
    private SterlingEngine _engine;
    private ISterlingDatabaseInstance _database;

    [TestInitialize]
    public void Init()
    {
      _engine = new SterlingEngine();
      _engine.Activate();
      // Also fails when using memory storage, but you must remove explicit calls to Init and Shutdown
      // in the test methods.
      _database = _engine.SterlingDatabase.RegisterDatabase<SameInstancesDatabase>( new IsolatedStorageDriver() );
    }

    [TestCleanup]
    public void Shutdown()
    {
      if( _engine != null )
      {
        _engine.Dispose();
        _engine = null;
        _database = null;
      }
    }

    [TestMethod]
    public void TestAddBill()
    {
      _database.Purge();

      Bill bill = new Bill()
      {
        Id = Guid.NewGuid(),
        Name = "Test"
      };

      _database.Save( bill );
      _database.Flush();
      // TODO: twice?

      Person person1 = new Person()
      {
        Id = Guid.NewGuid(),
        Name = "Martin"
      };

      _database.Save( person1 );
      _database.Flush();

      Partaker partaker1 = new Partaker()
      {
        Id = Guid.NewGuid(),
        Paid = 42,
        Person = person1
      };

      bill.Partakers.Add( partaker1 );

      _database.Save( bill );
      _database.Flush();
      // TODO: twice?

      Person person2 = new Person()
      {
        Id = Guid.NewGuid(),
        Name = "Jeremy"
      };

      _database.Save( person2 );
      _database.Flush();

      Partaker partaker2 = new Partaker()
      {
        Id = Guid.NewGuid(),
        Paid = 0,
        Person = person2
      };

      bill.Partakers.Add( partaker2 );

      _database.Save( bill );
      _database.Flush();
      // TODO: twice?

      List<TableKey<Bill, Guid>> billKeys = _database.Query<Bill, Guid>();

      Assert.IsTrue( billKeys.Count == 1 );
      Assert.AreEqual( billKeys[ 0 ].Key, bill.Id );

      // REMOVE THESE TWO CALLS IF TESTING MEMORY DRIVER.
      this.Shutdown();
      this.Init();

      // Check ids
      billKeys = _database.Query<Bill, Guid>();

      Assert.IsTrue( billKeys.Count == 1 );
      Assert.AreEqual( billKeys[ 0 ].Key, bill.Id );

      Bill freshBill = billKeys[ 0 ].LazyValue.Value;

      Assert.IsTrue( freshBill.Partakers.Count == 2 );

      // Order may vary.
      int partakerIndex = 0;

      if( freshBill.Partakers[ 0 ].Id == partaker1.Id )
      {
        Assert.AreEqual( freshBill.Partakers[ 0 ].Id, partaker1.Id ); // obviously
        Assert.AreEqual( freshBill.Partakers[ 1 ].Id, partaker2.Id );
      }
      else
      {
        Assert.AreEqual( freshBill.Partakers[ 1 ].Id, partaker1.Id );
        Assert.AreEqual( freshBill.Partakers[ 0 ].Id, partaker2.Id );
        partakerIndex = 1;
      }

      Assert.AreEqual( freshBill.Partakers[ partakerIndex ].Person.Id, person1.Id );
      Assert.AreEqual( freshBill.Partakers[ 1 - partakerIndex ].Person.Id, person2.Id );

      List<TableKey<Person, Guid>> personKeys = _database.Query<Person, Guid>();

      Assert.IsTrue( personKeys.Count == 2 );

      // Order may vary
      int personIndex = 0;

      if( personKeys[ 0 ].Key == person1.Id )
      {
        Assert.AreEqual( personKeys[ 0 ].Key, person1.Id ); // obviously
        Assert.AreEqual( personKeys[ 1 ].Key, person2.Id );
      }
      else
      {
        Assert.AreEqual( personKeys[ 1 ].Key, person1.Id );
        Assert.AreEqual( personKeys[ 0 ].Key, person2.Id );
        personIndex = 1;
      }

      Person freshPerson1 = personKeys[ personIndex ].LazyValue.Value;
      Person freshPerson2 = personKeys[ 1 - personIndex ].LazyValue.Value;

      // Compare fresh instances. Ids AND instances must match.
      Assert.AreEqual( freshBill.Partakers[ partakerIndex ].Person.Id, freshPerson1.Id );
      Assert.AreEqual( freshBill.Partakers[ 1 - partakerIndex ].Person.Id, freshPerson2.Id );

      // This currently fails.
      Assert.AreEqual( freshBill.Partakers[ partakerIndex ].Person, freshPerson1 );
      Assert.AreEqual( freshBill.Partakers[ 1 - partakerIndex ].Person, freshPerson2 );
    }
  }
}

The three classes (and their common base) are pretty straightforward. The redundant calls to save and flush are there to reproduce what can happen with the real app. It might be a clue for the problem. At the very end, you can see that loading a Bill, with its Partaker and Person does not load the same instances as loading the Person directly. I can trace and see the call to cache.CheckKey return null in BaseDatabaseInstance.Load.

Coordinator
May 22, 2011 at 6:59 PM

OK, I see what you are driving at. I thought there was an issue with how it was saving, but you are looking for a "reference equals" or expecting the exact same instance to come out.

That's definitely NOT how Sterling works or is expected to work.

Sterling does not cache all instances as they go in - that would be insane, as then if you set up a database with 1,000 items, there would be 1,000.

The keys are lazy loaded - when you get a query, and you access the value, it triggers a load from disk. That will NOT give you your exact object instance you sent in, but rather a copy that it pulls from the persisted store. If you don't modify anything and query that same key list, then the lazy value will resolve to the same instance that was loaded back (again, not the instance you sent in). If you however insert/save/update that will invalidate the cash. As it should - again, if Sterling kept everything (object-instance-wise) in memory it would grow uncontrollably, so it has a light cache to accommodate that. The behavior is as expected.

May 22, 2011 at 10:24 PM

Thanks for the clarification.

Just to make sure: Loading A (which references B, itself referencing C) versus loading C, this will create two distinct instances of C, even if it's the same key. In my example, I'm comparing loaded instances only, and I'm not modifying them in any way.

Coordinator
May 22, 2011 at 10:29 PM

Yes, currently that is the behavior - I understand why it's not desired, in case "C" shows up multiple times in the tree, so I will look into addressing it, but currently only the direct key lazy load would cache a value, any type of hierarchical load will reload any time it is found in the tree. Will definitely look into this to see if there is a fix for it I can make for 1.5 because again I understand the desired behavior is that if you modify one C, you modify them all.

May 22, 2011 at 10:50 PM

Thanks!

(and thanks for the quick answer!)

Coordinator
May 29, 2011 at 5:01 PM

This has been addressed although not in the way you might desire or expect.

Here's the caveat: Sterling cannot afford to cache everything in memory. I'll release an object-based "lite" version eventually that does that, but even with the memory driver, it's the byte images, not he objects, that are saved. The reason is straightforward - if you are dealing with 10,000 objects the last thing you want is 10,000 instances so you are not guaranteed to have the same instance when you load the same id because it generates a new instance.

However, you should be guaranteed the same instance in the same object graph. What I mean is if you have a "foreign table" called Person, and the person exists in two or three different sub classes of the parent, then loading the parent should result in those sub classes being equal. I've modified the code and added a new test that verifies this indeed does happen.

Hope that helps!