tags:

views:

24

answers:

1

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.

+1  A: 

I find that for thick client or desktop applications the approach to NHibernate session management detailed here is the only "workable" solution. The jist of this solution is to keep most objects detached from a session unless they:

  1. are being actively edited by a user
  2. have been updated elsewhere in the app. Use events to notify other view that they need to refresh their data

Note that I couldn't get stateless sessions to work per the MSDN article so I just use a normal session and then clear it when my transaction completes.

David Lynch