I'm building a standard old thick client with NHibernate where the majority of my entities (see class A
) are dehydrated at application start and stay there for the app lifetime. These occassionally reference are a couple of very heavyweight classes (class B
) that encapsulate a lot of data which are lazily loaded proxies so that they are only really fetched from the database on demand.
This works fine. The problem is I am wanting to also explicitly unload these B types later - from my point of view I want to go back to the state when they were uninitialized proxies and not taking up memory.
Seeing as there doesn't seem to be a way to de-initialize a proxy, I was planning on simply evicting B
from the cache, removing all existing references to B
(I realise I have to track them myself, this is fine) by replacing them with a new (uninitialized) B-proxy obtained from session.Load<B>
(ie a.MyB = session.Load<B>(oldId);
)
However it seems like NHibernate keeps a reference to the old B
in the cache, and B
won't be garbage collected until I either remove A
from the cache or flushing the session.
I'm not sure why. I suspect it keeps the reference for the purposes of property comparison so it can see if it needs to push back to the database if I ever request a session.SaveOrUpdate(a)
. But if have set the relationship to not cascade so should it really matter?
The fluent mappings & tests bellow illustrate the matter. Use Make()
to make the database and then Load()
to run the test. Note that if you do uncomment the flush you will need to re-Make()
again.
public class A
{
internal int Id { get; private set; }
public B B { get; set; }
internal class Map : ClassMap<A>
{
public Map()
{
Id(x => x.Id);
Not.LazyLoad();
References(x => x.B)
.Cascade.None()
.LazyLoad(Laziness.Proxy);
}
}
}
public class B
{
protected internal virtual int Id { get; private set; }
public virtual A A { get; set; }
public virtual void Fizz()
{
}
internal class Map : ClassMap<B>
{
public Map()
{
Id(x => x.Id);
LazyLoad();
References(x => x.A)
.Cascade.None();
}
}
private static ISessionFactory BuildSF(bool rebuilddb)
{
return Fluently.Configure()
.Database(SQLiteConfiguration.Standard.UsingFile("v.sqldb"))
.Mappings(m =>
{
m.FluentMappings.Add<A.Map>();
m.FluentMappings.Add<B.Map>();
})
.ExposeConfiguration(cfg =>
{
//cfg.Interceptor = new Interceptor();
if (rebuilddb)
new SchemaExport(cfg).Create(false, true);
})
.BuildSessionFactory();
}
[Test]
public void Make()
{
var sessionFactory = BuildSF(true);
var sess = sessionFactory.OpenSession();
var a = new A();
var b = new B();
a.B = b;
b.A = a;
sess.Save(a);
sess.Save(b);
sess.Flush();
}
[Test]
public void Load()
{
var sessionFactory = BuildSF(false);
var sess = sessionFactory.OpenSession();
var a = sess.Load<A>(1);
// just loaded a, B should be a uninitialized proxy
Assert.That(! NHibernateUtil.IsInitialized(a.B));
a.B.Fizz();
// invoke on B, should now be initialized
Assert.That(NHibernateUtil.IsInitialized(a.B));
var weakB = new WeakReference(a.B);
sess.Evict(a.B);
//sess.Evict(a); // uncomment this to pass the final test
a.B = null;
//sess.Flush(); // or this
System.GC.Collect();
Console.WriteLine("Entities: " + sess.Statistics.EntityCount);
foreach (var ek in sess.Statistics.EntityKeys)
Console.WriteLine("\t" + ek.EntityName + " " + ek.Identifier);
if (sess.Contains(a))
Console.WriteLine("Session still contains a in cache");
else
Console.WriteLine("Session no longer holds a");
Assert.IsFalse(weakB.IsAlive);
}
On the lower level my question is: can I remove this reference to the old B in some way without either removing A or flush?
On a higher level my question: how can I do what I want? My immediate thought to workaround this was to maybe just map and id on the A class and manually manage persistence/depersistence of B. But of course this bleeds persistence issues into my domain model a little, and my experience has been trying that to apply 'hacks' to NHibernate is usually a 'alice in wonderland down the rabbit hole' experience.