New Posts New Posts RSS Feed: Cache not hit on retrieve
  FAQ FAQ  Forum Search   Calendar   Register Register  Login Login

Cache not hit on retrieve

 Post Reply Post Reply
Author
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Topic: Cache not hit on retrieve
    Posted: 13-Nov-2009 at 9:47am
I mentioned unit tests that confirm the effect of EntityManager.DefaultQueryStrategy on navigations. Here are tests

The tests are called within a test class that constructs a new EntityManager (_manager) for each test. "ModelB" defines Product, Category, and Supplier entities for the NorthwindIB database. Every Product has a Category; every Category can navigate to all the Products that have it as their Category. The ModelBScenarios methods simple get the specified Product or Category from the database with a simple LINQ query.

I hope it is clear in these tests that changing the DefaultQueryStrategy just prior to invoking the navigation property determines whether DevForce satisfies the property from cache or the database.
 
    [TestMethod]
    public void ShouldNotGoToDbForProductCategoryWhenEMdefaultQueryStrategyIsCacheOnly() {
      var category = GetProductCategoryGivenQueryStrategy(QueryStrategy.CacheOnly);
      DFAssert.IsNulloEntity(category);
    }
 
    [TestMethod]
    public void ShouldGoToDbForProductCategoryWhenEMdefaultQueryStrategyIsNormal() {
      var category = GetProductCategoryGivenQueryStrategy(QueryStrategy.Normal);
      DFAssert.IsRealEntity(category);
    }
    private ModelB.Category GetProductCategoryGivenQueryStrategy(QueryStrategy strategy) {
      var product = ModelBScenarios.GetSauerkrautProduct(_manager);
      _manager.DefaultQueryStrategy = strategy;
      return product.Category;
    }
 
    [TestMethod]
    public void ShouldNotGoToDbForCategoryProductsWhenEMdefaultQueryStrategyIsCacheOnly() {
      var products = GetCategoryProductsGivenQueryStrategy(QueryStrategy.CacheOnly);
      Assert.IsTrue(0 == products.Count);
    }
 
    [TestMethod]
    public void ShouldGoToDbForCategoryProductsWhenEMdefaultQueryStrategyIsNormal() {
      var products = GetCategoryProductsGivenQueryStrategy(QueryStrategy.Normal);
      Assert.IsTrue(0 < products.Count);
    }
 
    private IList<ModelB.Product> GetCategoryProductsGivenQueryStrategy(QueryStrategy strategy) {
      var category = ModelBScenarios.GetBeverageCategory(_manager);
      _manager.DefaultQueryStrategy = strategy;
      return category.Products;
    }


Edited by WardBell - 13-Nov-2009 at 9:49am
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 13-Nov-2009 at 9:34am
That's clarifying, Paul, thank you.
 
I do not understand your statement - "although we tell the EntityManager to use the 'CacheOnly' strategy, DevForce still goes to the DB for some of the Entities."
 
Setting the EntityManager.DefaultQueryStrategy to "CacheOnly" prevents *all* queries (including navigations) from going to the db unless the query (or navigation) itself explicitly requests a server trip. I have unit tests that confirm these assertions for navigations. I welcome a test from you that shows otherwise.
 
--
 
If I understand your scenario, certain navigations should always be satisfied from cache. In your example, you expect that somePerson.LastNames will retrieve the LastName entites from cache, having previously pre-fetched the ones associated with that Person entity. You are saying that the LastNames property should always work that way.
 
In this situation, I might use the feature I described above, the one that instructs the navigation to confine itself to the cache:
During your setup phase, you could call something like this:
 
    var strategy = new EntityReferenceStrategy(EntityReferenceLoadStrategy.DoNotLoad, MergeStrategy.PreserveChanges);
    Person.LastNamesEntityProperty.RelationLink.ToRole.ReferenceStrategy = strategy;
 
Apparently you have the metadata necessary to generate this special purpose initialization.
 
Alternatively, you could create a LastNamesInCache property that does the equivalent cache-only query.
 
--
 
I would bet that you do not fetch ALL LastName entities for all persons in your database. Therefore, it could be dangerous to use a feature that enabled you to declare that "all LastNames are in cache" - even if we chose to add such a feature (hint hint).
 
I can say with considerable certainty that we are not adding a feature that can determine if an arbitrary query covers another arbitrary query (e.g., that a query for "the integer 2 and all odd integers" covers a query for "all prime numbers"); this kind of thing is beyond our skill.
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 13-Nov-2009 at 5:18am
Hi Ward,
 
Saw you at Microsoft a while ago. Good show..
 
Let me first tell you about our project. We use the T4 and SFS generation tools to generate our ViewModel as well as class enhancements for the DevForce partial classes. This is why I stated 'Generated from our Model'.
 
Aha. I see your point. That's sad. DevForce does not see the more specific queries with a where clause as already covered by the 'get all' queries. It would be a great enhancement if it did ;-)
 
When we query the master we do not query the detail (Navigation) properties. They are lazy loaded only when needed. Let's say we have a Person which has 1 or more LastNames. Only 1 name is valid at one time which is determined by it ValidFrom/To Dates. In the Person class we hand-coded a routine that gets the FullName (calculated field). This FullName gets the CurrentLastName from the Navigation property LastName of Person. FirstName is in Person itself and so FullName is composed of the LastName from the Navigation property, a ', ' and the FirstName from Person.
 
At startup we fetch all Persons that are in our DB and we would like to display the FullName for all occurences. When we do not pre-load the LastNames in the DB this takes forever, becaus DevForce will go to the DB for each LastName (figures).
 
In our generated code we recognize tables that are pre-loaded and the ones that are not. For the tables that are pre-loaded we generate the code in the first code snippet above. These tables are always queried with the 'CacheOnly' strategy as you can see in the second code snippit. Tables that are not pre-loaded always use the 'Normal' querystratgy.
 
We can assume that the data is in the cache always because we use the PushNotification to update the cache for any entities that were updated on the server. Do do that we gather all the saving keys in the 'OnSaving' routine and send them to all the clients in the 'OnSaved' routine. All the clients then update there cache using (for the moment) the refetch of all entities with the keys pushed from the server (we do have a problem with the refetch at the moment but we're confident this will be resolved before we go into production, early next year).
 
What we see is the following. Although we pre-loaded many of the Entities in the startup of the program and although we tell the EntityManager to use the 'CacheOnly' strategy, DevForce still goes to the DB for some of the Entities. We've not digged into this problem deeply, because of lack of time. We realy did not expect DevForce to go to DB for these Entities because of the fact that they are in cache already and we specify 'CacheOnly' on the EntityManager.
 
Regards,
Paul Sinnema
Diartis AG
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 11-Nov-2009 at 2:08pm

Hi Paul - Ton of material here. Not entirely sure about your question. I think you are wondering why navigation properties that return lists of entities do not seem to be aware that the material they need is already in cache.

Let's simplify. Suppose you had only four kinds of entities: Product, Color, Supplier, and ProductSupplierMap. You pre-filled the cache with Colors and Suppliers because these are reference entities: there are a handful of each and they are stable. You pre-filled the cache with Products and their ProductSupplierMaps (entities which relate a Product to one or more Suppliers). It would seem that everything that you could possible need to move among these four entity types is waiting for you in cache.
 
The Product has two navigation properties: Color and ProductSupplierMaps.
 
The Color property will always be satisfied from the cache because the Product entity holds a foreign key value, the ID of the Color; when you call "aProduct.Color", DevForce looks in the cache for the Color with that product's ColorID, finds it, and returns the Color object.
 
So it is not true that for "all Navigation Properties ... DevForce still goes to the the DB to fetch the data". Most scalar navigation properties - properties that(a) return 0 or 1 entity - will try to satisfy requests only from the cache. The exception is the "master" entity in a 1-to-1 relationship ... but that's a story for another time.
 
However, a call to "aProduct.ProductSupplierMaps" will go to the database. "Why?" you ask; "all ProductSupplierMaps are in cache - they are readily available - shouldn't DevForce know that?"
 
Maybe it should. But it doesn't. There is no information available to the EntityManager that tells it that all ProductSupplierMaps are in cache. YOU know that because you queried for them. And if you issued another query for all ProductSupplierMaps, the EntityManager would dutifully respond entirely from the cache. It can do that because it has seen the query "get all ProductSupplierMaps" before.
 
But when DevForce sees "aProduct.ProductSupplierMaps" for the first time, it translates this to the query "get all ProductSupplierMaps where the map.ProductId equals this product's Id".  This is the first time we've asked for this product's ProductSupplierMaps. The EntityManager has not seen this particular query before. So it must go to the database.
 
DevForce cannot reason that this second query is conceptually covered by the "get all ProductSupplierMaps" query.
 
We've thought over the years about extending DevForce so that it could draw that conclusion. We think there are "Enumeration" entities (like "Color") for which this kind of special treatment would be worthwhile. But we have not decided in favor of that extension *just yet*.
 
Therefore, the rule is: "all list navigation properties (those potentially returning more than one entity) will cause DevForce to fetch from the database the first time ... unless the EntityManager's DefaultQueryStrategy instructs otherwise ... or the EntityManager is disconnected."
--
YOU know that the second query is covered by the "get all" query. What can you do?
 
You could change the DefaultQueryStrategy to CacheOnly; when you want a query to go to the database, you state so explicitly on the query itself. Otherwise all queries - and all navigations - are satisfied locally. This is the approach I recommend for Silverlight applications because you want to keep your awareness of server trips very high and very explicit.
 
You could add your own "ProductSupplierMapsFromCache" property that did exactly what you want.
 
You can change the way the ProductSupplierMaps behaves. This is an advanced feature that few know about.
 
We generate navigation properties to behave in a "normal way" but you can change that. If you look inside the generated code for this property, you will see a reference to a static field "ProductSupplierMapsEntityProperty". This field is of type NavigationEntityProperty. You are free to change some of its properties.
 
During your setup phase, you could call something like this:
 
    var strategy = new EntityReferenceStrategy(EntityReferenceLoadStrategy.DoNotLoad, MergeStrategy.PreserveChanges);
    Product.ProductSupplierMapsEntityProperty.RelationLink.ToRole.ReferenceStrategy = strategy;
 
Henceforth, the Product.ProductSupplierMaps navigation property will always attempt to satisfy the request from the cache because you said "DoNotLoad".
 
I'm not sure I like this last approach because it is so unexpected; it's there for you if you like it.
 
---
p.s.: There are some other curiosities in your post. The one I want to comment about is the statement "generated code from our Model". I'm not sure who did the generating but it certainly wasn't DevForce. I've never seen code like this before.
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 10-Nov-2009 at 11:13pm
Hi,
 
Maybe someone can clarify to what the problem is here. We've got a performance problem with the cache. To improve on performance we pre-read some of our entities like so (is generated code):
 
  /// <summary>
  /// Pre-load all the Navigation properties
  /// </summary>
  public void LoadAllNavigationProperties()
  {
   ModelContext.Instance.KLIBDefaultManager.CivilStatusType.ToList();
   ModelContext.Instance.KLIBDefaultManager.Person.ToList();
   ModelContext.Instance.KLIBDefaultManager.SeparationType.ToList();
   ModelContext.Instance.KLIBDefaultManager.CivilStatus.ToList();
   ModelContext.Instance.KLIBDefaultManager.CorrespondenceLanguage.ToList();
   ModelContext.Instance.KLIBDefaultManager.Address.ToList();
   ModelContext.Instance.KLIBDefaultManager.Communication.ToList();
   ModelContext.Instance.KLIBDefaultManager.Contact.ToList();
   ModelContext.Instance.KLIBDefaultManager.Nationality.ToList();
   ModelContext.Instance.KLIBDefaultManager.PostalCode.ToList();
   ModelContext.Instance.KLIBDefaultManager.Country.ToList();
   ModelContext.Instance.KLIBDefaultManager.Gender.ToList();
   ModelContext.Instance.KLIBDefaultManager.LastName.ToList();
   ModelContext.Instance.KLIBDefaultManager.AddressType.ToList();
   ModelContext.Instance.KLIBDefaultManager.CommunicationType.ToList();
   ModelContext.Instance.KLIBDefaultManager.ContactRelationRoleType.ToList();
   ModelContext.Instance.KLIBDefaultManager.Relation.ToList();
   ModelContext.Instance.KLIBDefaultManager.RelationType.ToList();
   ModelContext.Instance.KLIBDefaultManager.Organization.ToList();
  }
 }
 
Excerpt from the ModelContext:
 
  /// <summary>
  /// This is the main manager for DevForce. It is a singleton pattern that creates a new KLIB Entity Manager
  /// </summary>
  public KLIBEntityManager KLIBDefaultManager
  {
   get
   {
    if (m_KLIBEntityManager == null)
    {
     m_KLIBEntityManager = new KLIBEntityManager(true);
     m_KLIBEntityManager.Fetched += new EventHandler<EntityFetchedEventArgs>(KLIBEntityManagerFetched);
     m_KLIBEntityManager.Saving += new EventHandler<EntitySavingEventArgs>(KLIBEntityManagerSaving);
     m_KLIBEntityManager.Saved += new EventHandler<EntitySavedEventArgs>(KLIBEntityManagerSaved);
     m_KLIBEntityManager.LoadAllNavigationProperties();  // Recursion!
    }
    return m_KLIBEntityManager;
   }
  }
 
Generated code from our Model:
 
       /// <summary>
        /// Gets the Address navigation items from DB. Is never 'null' so items can be added easily.
        /// </summary>
        [DebuggerNonUserCode]
        public AbstractIntersectingHistoryList<Address> AddressList
        {
   get
   {
    if (m_AddressList == null)
    {
     if (m_Local == 0)
     {
      QueryStrategy queryStrategy = null;
      
      m_Local++;
      try
      {
       queryStrategy = ModelContext.Instance.KLIBDefaultManager.DefaultQueryStrategy;
       
       // Presume the data is already in the cache
       ModelContext.Instance.KLIBDefaultManager.DefaultQueryStrategy = QueryStrategy.CacheOnly;
       // If nothing is found in the cache, restore the queryStrategy
       if (AddressKLIBAlias.Count() == 0)
       {
        ModelContext.Instance.KLIBDefaultManager.DefaultQueryStrategy = queryStrategy;
       }
       m_AddressList = new AbstractIntersectingHistoryList<Address>(AddressKLIBAlias);
       AddEmptyAddress();
       m_AddressList.ItemChanged += OnNavigationListItemChanged;
       m_AddressList.ListChangedObservable += OnNavigationListChanged;       
       OnListFetched(this, new ListFetchedEventArgs("AddressList", m_AddressList));
      }
      catch
      {
       throw;
      }
      finally
      {
       // Restore the Querystrategy no matter what
       ModelContext.Instance.KLIBDefaultManager.DefaultQueryStrategy = queryStrategy;
       m_Local--;
      }
     }
    }
    return m_AddressList;
   }
  }
 
These entities are all in the cache after this has been executed (and DevForce does a fine job here, because it is pretty fast). What we see however is that for all Navigation Properties (i.e. CivilStatus, Country, SeperationType, etc. from the list above) DevForce still goes to the DB to fetch the data, causing fetching of data to be exstremely slow.
 
Am I wrong in assuming that this is in fact wrong behavior. Shouldn't the pre-read entities come from the cache for Navigation Properties? We are on the same EntityManager (i.e. the same cache).
 
Regards,
Paul Sinnema
Diartis AG
 


Edited by Sinnema - 11-Nov-2009 at 12:00am
Back to Top
 Post Reply Post Reply

Forum Jump Forum Permissions View Drop Down