NHibernate second-level caching - evicting regions - nhibernate

We have a number of cache regions set up in our nHibernate implementation. In order to avoid trouble with load balanced web servers, I want to effectively disable the caching on the pages that edit the cached data. I can write a method that clears out all my query caches, my class caches and my entity caches easily enough.
But what I really want is to clear the cache by region. sessionFactory.EvictQueries() will take a region parameter, but Evict() and EvictCollection() does not. I don't really want to throw away the whole cache here, nor do I want to maintain some sort of clumsy dictionary associating types with their cache regions. Does nHibernate have a way to ask an entity or collection what its caching settings are?
thanks

I've just done the same thing. For everyone's benefit, here is the method I constructed:
public void ClearCache(string regionName)
{
// Use your favourite IOC to get to the session factory
var sessionFactory = ObjectFactory.GetInstance<ISessionFactory>();
sessionFactory.EvictQueries(regionName);
foreach (var collectionMetaData in sessionFactory.GetAllCollectionMetadata().Values)
{
var collectionPersister = collectionMetaData as NHibernate.Persister.Collection.ICollectionPersister;
if (collectionPersister != null)
{
if ((collectionPersister.Cache != null) && (collectionPersister.Cache.RegionName == regionName))
{
sessionFactory.EvictCollection(collectionPersister.Role);
}
}
}
foreach (var classMetaData in sessionFactory.GetAllClassMetadata().Values)
{
var entityPersister = classMetaData as NHibernate.Persister.Entity.IEntityPersister;
if (entityPersister != null)
{
if ((entityPersister.Cache != null) && (entityPersister.Cache.RegionName == regionName))
{
sessionFactory.EvictEntity(entityPersister.EntityName);
}
}
}
}

OK, looks like i've answered my own question. The default interface that's returned when you pull out the nHibernate metadata doesn't provide information on caching, however if you dig around in the implementations of it, it does. A bit clumsy, but it does the job.

Related

Nhibernate Clear Cache for a Specific Region

I am trying to manually clear the level 2 cache for a specific region. I found the method posted in answer to this question. While this is working to clear my entities, for some reason the querycache is not getting cleared. This results in a separate query for each entity the next time the entities are retrieved from the database. If does work when I call sessionFactory.EvictQueries() without any parameters. It is only not working when I am passing in a specific region name. Any ideas as to what is going wrong?
Code is from the above link:
private void ClearRegion(string regionName)
{
_sessionFactory.EvictQueries(regionName);
foreach (var collectionMetaData in _sessionFactory.GetAllCollectionMetadata().Values)
{
var collectionPersister = collectionMetaData as NHibernate.Persister.Collection.ICollectionPersister;
if (collectionPersister != null)
{
if ((collectionPersister.Cache != null) && (collectionPersister.Cache.RegionName == regionName))
{
_sessionFactory.EvictCollection(collectionPersister.Role);
}
}
}
foreach (var classMetaData in _sessionFactory.GetAllClassMetadata().Values)
{
var entityPersister = classMetaData as NHibernate.Persister.Entity.IEntityPersister;
if (entityPersister != null)
{
if ((entityPersister.Cache != null) && (entityPersister.Cache.RegionName == regionName))
{
_sessionFactory.EvictEntity(entityPersister.EntityName);
}
}
}
}
Caching is working and verified using NHProfiler.
Ok, so I figured out my issue. I did not realize that it is necessary to specify a cache region when querying the data, aside from specifying it in the entity mapping. After adding .CacheRegion("regionName") to my queries everything works. By not adding the region when querying, it was going into the query cache without a region name. That is why it worked when I called.EvictQueries() without a region name parameter.
To sum it up, it is necessary to add the region name when mapping the entities (.Region("regionName") when using Fluent) and when querying with isession using .CacheRegion("regionName").
Thank you for you responses.

Change tracking aggregate root in DDD

This question is largely based on the article NHibernate – Automatic change tracking for aggregate roots in DDD scenarios
Although the logic in the article seems sound I have yet to find an implementation solution that will cover all use cases.
The problem seems to be related to the following paragraph from the article
There is a slight problem here that we may generate several updates per transaction here, but I am not worried about that overly much, it is fairly simple to resolve (by keeping track of the entity and not updating if we already updated in the current transaction), so I’ll leave it up to you.
Following the article we simply force a version update whenever we update a related entity within the aggregate root. However in cases where both aggregate root and related entity are 'dirty' this will cause a double update on the aggregate root. This causes nhibernate to fall over as the second version update triggered by default from the dirty aggregate root expects the version to be the same as what was loaded from the db.
I've attempted to put a check into the 'PreInsertEventListener' and 'PreUpdateEventListener' checking if the aggregate root is dirty when updating a related entity. If this is the case then ignore the forced update of version.
public bool OnPreUpdate(PreUpdateEvent updateEvent)
{
var rootFinder = updateEvent.Entity as ICanFindMyAggregateRoot;
if (rootFinder == null)
return false;
if (!updateEvent.Session.IsAggregateRootDirty(rootFinder.MyRoot))
{
updateEvent.Session.Lock(rootFinder.MyRoot, LockMode.Force);
}
return false;
}
public static class SessionExtensions
{
public static bool IsAggregateRootDirty(this ISession session, IAggregateRoot entity)
{
ISessionImplementor sessionImplementation = session.GetSessionImplementation();
IPersistenceContext persistenceContext = sessionImplementation.PersistenceContext;
IEntityPersister entityPersister = sessionImplementation.GetEntityPersister(null, entity);
EntityEntry entityEntry = persistenceContext.GetEntry(entity);
if ((entityEntry == null) && (entity is INHibernateProxy))
{
INHibernateProxy proxy = entity as INHibernateProxy;
object obj = sessionImplementation.PersistenceContext.Unproxy(proxy);
entityEntry = sessionImplementation.PersistenceContext.GetEntry(obj);
}
object[] oldState = entityEntry.LoadedState;
object[] currentState = entityPersister.GetPropertyValues(entity, sessionImplementation.EntityMode);
int[] findDirty = entityEntry.Persister.FindDirty(currentState, oldState, entity, sessionImplementation);
var hasDirtyCollection = currentState.OfType<IPersistentCollection>().Any(x => x.IsDirty);
return (findDirty != null) || hasDirtyCollection;
}
}
This solution does seem to work albeit I still need to test it with few more use cases. However I feel as if this solution is a bit heavy handed and was hoping for a solution more along what was outlined in the article.
Is there a way to detect weather the version has already been updated in the same transaction or will be, or a simple way to keep track of entities within the transaction set have its version updated.
Thanks.

Getting a list of classes and collections that have second level caching enabled

Given a reference to an ISessionFactory instance, is it possible to get a list of all classes and collections which have second level caching enabled for them?
Edit:
To give a little more context for what I'm trying to achieve:
I'd like to build an administration interface that can provide the ability to flush portions of the second level cache using sessionFactory.Evict and sessionFactory.EvictCollection. More information can be found here.
For example:
Flush all entities of this type
Flush this specific entity
Flush all collections of this entity
Flush this specific collection of entities
For this, I will need a way to dynamically list the entity and entity collection types that are available for performing flush operations on.
You can use the GetAllClassMetadata() and the GetAllCollectionMetadata() methods the get the metadata from the persistent clasess and the collections.
The trick is that you need to cast the returned metadata classes to IEntityPersister and ICollectionPersister where you can now check for the Cache property which should not be null if caching is configured for the entity/collection.
So your could will look like this
var cachedEnityTypes = new List<string>();
var cachedCollectionRoles = new List<string>();
foreach (var classMetadata in sessionFactory.GetAllClassMetadata())
{
var persister = classMetadata.Value as IEntityPersister;
if (persister != null && persister.Cache != null)
cachedEnityTypes.Add(persister.EntityName);
}
foreach (var collectionMetadata in nHibernateSessionFactory.GetAllCollectionMetadata())
{
var persister = collectionMetadata.Value as ICollectionPersister;
if (persister != null && persister.Cache != null)
cachedCollectionRoles.Add(collectionMetadata.Value.Role);
}

Updating complex type with ef code first

I have a complex type called account, which contains a list of licenses.
Licenses in turn contains a list of domains (a domain is a simple id + url string).
In my repository I have this code
public void SaveLicense(int accountId, License item)
{
Account account = GetById(accountId);
if (account == null)
{
return;
}
if (item.Id == 0)
{
account.Licenses.Add(item);
}
else
{
ActiveContext.Entry(item).State = EntityState.Modified;
}
ActiveContext.SaveChanges();
}
When I try to save an updated License (with modified domains) what happens is that strings belonging straight to the license get updated just fine.
However no domains get updated.
I should mention that what I have done is allow the user to add and remove domains in the user interface. Any new domains get id=0 and any deleted domains are simply not in the list.
so what I want is
Any domains that are in the list and database and NOT changed - nothing happens
Any domains that are in the list and database, but changed in the list - database gets updated
Any domains with id=0 should be inserted (added) into database
Any domains NOT in the list but that are in the database should be removed
I have played a bit with it with no success but I have a sneaky suspicion that I am doing something wrong in the bigger picture so I would love tips on if I am misunderstanding something design-wise or simply just missed something.
Unfortunately updating object graphs - entities with other related entities - is a rather difficult task and there is no very sophisticated support from Entity Framework to make it easy.
The problem is that setting the state of an entity to Modified (or generally to any other state) only influences the entity that you pass into DbContext.Entry and only its scalar properties. It has no effect on its navigation properties and related entities.
You must handle this object graph update manually by loading the entity that is currently stored in the database including the related entities and by merging all changes you have done in the UI into that original graph. Your else case could then look like this:
//...
else
{
var licenseInDb = ActiveContext.Licenses.Include(l => l.Domains)
.SingleOrDefault(l => l.Id == item.Id)
if (licenseInDb != null)
{
// Update the license (only its scalar properties)
ActiveContext.Entry(licenseInDb).CurrentValus.SetValues(item);
// Delete domains from DB that have been deleted in UI
foreach (var domainInDb in licenseInDb.Domains.ToList())
if (!item.Domains.Any(d => d.Id == domainInDb.Id))
ActiveContext.Domains.Remove(domainInDb);
foreach (var domain in item.Domains)
{
var domainInDb = licenseInDb.Domains
.SingleOrDefault(d => d.Id == domain.Id);
if (domainInDb != null)
// Update existing domains
ActiveContext.Entry(domainInDb).CurrentValus.SetValues(domain);
else
// Insert new domains
licenseInDb.Domains.Add(domain);
}
}
}
ActiveContext.SaveChanges();
//...
You can also try out this project called "GraphDiff" which intends to do this work in a generic way for arbitrary detached object graphs.
The alternative is to track all changes in some custom fields in the UI layer and then evaluate the tracked state changes when the data get posted back to set the appropriate entity states. Because you are in a web application it basically means that you have to track changes in the browser (most likely requiring some Javascript) while the user changes values, adds new items or deletes items. In my opinion this solution is even more difficult to implement.
This should be enough to do what you are looking to do. Let me know if you have more questions about the code.
public void SaveLicense(License item)
{
if (account == null)
{
context.Licenses.Add(item);
}
else if (item.Id > 0)
{
var currentItem = context.Licenses
.Single(t => t.Id == item.Id);
context.Entry(currentItem ).CurrentValues.SetValues(item);
}
ActiveContext.SaveChanges();
}

How to get list of changed (dirty) entities from Nhibernate session?

I need to write some business logic rigt before flush against all changed entities. One of the solution I've tried is IPreUpdateEventListener. But this event listener already have object denormalized to key-value. I need something before denormalization and even before flush.
So the questions is how to get list of changed (diry) entities.
My code to detect dirty entities
var dirtyObjects = new List<object>();
var sessionImpl = hsession.GetSessionImplementation();
foreach (NHibernate.Engine.EntityEntry entityEntry in sessionImpl.PersistenceContext.EntityEntries.Values)
{
var loadedState = entityEntry.LoadedState;
var o = sessionImpl.PersistenceContext.GetEntity(entityEntry.EntityKey);
var currentState = entityEntry.Persister.GetPropertyValues(o, sessionImpl.EntityMode);
if (entityEntry.Persister.FindDirty(currentState, loadedState, o, sessionImpl) != null)
{
dirtyObjects.Add(entityEntry);
}
}
You might look at the Flush event. But what are you trying to accomplish, exactly?
Well, best solution I've found is using PersistenceContext.
foreach (var entity in eventSource.PersistenceContext.EntityEntries.Keys)
{
// entity is entity to update...
}
Not sure if this is right solution, however.