Search code examples
nhibernatesecond-level-cache

nHibernate eager loading- strange update behaviour


my domain is an airport, which contains several terminals, and each terminal contains zones etc.
since the number of airport / terminal / zone entities is very small, i'd like to:
1. load all the heirarchy eagerly when retrieving an airport.
(using the following fluent configuration:

//eagerly load terminals
mapping.HasMany(x => x.Terminals).Not.LazyLoad()
            .Cache.ReadWrite();

)
2. enable 2nd level caching, so that all retrievals of an airport object would not hit the DB.

the eager loading and caching works fine, but the following test produces some strange behaviour.
(the following code retrieves an airport entity twice (not hitting the DB the second time), and updates one of them.)

        [TestMethod]
    public void TestSecondLevelCache()
    {
        Airport firstAirport = null, secondAirport = null;

        Console.WriteLine("first select");
        using (ISession session = this.SessionFactory.OpenSession())
        {
            using (ITransaction transaction = session.BeginTransaction())
            {
                //the idea here is to see whether there are two calls to DB here. check the sql output
                AirportDAO dao = new AirportDAO(session);
                firstAirport = dao.GetAirport();
                transaction.Commit();
            }
        }

        Console.WriteLine("second select");
        using (ISession session = this.SessionFactory.OpenSession())
        {
            using (ITransaction transaction = session.BeginTransaction())
            {
                //the idea here is to see whether there are two calls to DB here. check the sql output
                AirportDAO dao = new AirportDAO(session);
                secondAirport = dao.GetAirport();
                transaction.Commit();
            }
        }

        Console.WriteLine("Are those the same airport instance? " + firstAirport.Equals(secondAirport));

        Console.WriteLine("now adding a terminal");
        using (ISession session = this.SessionFactory.OpenSession())
        {
            using (ITransaction transaction = session.BeginTransaction())
            {
                secondAirport.Terminals.Add(new Terminal() { Name = "terminal added to second airport", Zones = new List<Zone>() });
                session.Update(secondAirport);
                transaction.Commit();
            }
        }
        //this Assert fails, since firstAirport != secondAirport
        Assert.IsNotNull(firstAirport.Terminals.FirstOrDefault(t => t.Name.Contains("second airport")));
    }

see the resulting output:

first select
NHibernate: SELECT airport0_.Id as Id36_0_, airport0_.Name as Name36_0_, airport0_.IsDeleted as IsDeleted36_0_ FROM dbo.[Airport] airport0_ WHERE airport0_.Id=@p0;@p0 = 1

NHibernate: SELECT terminals0_.Airport_id as Airport4_1_, terminals0_.Id as Id1_, terminals0_.Id as Id50_0_, terminals0_.Name as Name50_0_, terminals0_.IsDeleted as IsDeleted50_0_, terminals0_.Airport_id as Airport4_50_0_ FROM dbo.[Terminal] terminals0_ WHERE terminals0_.Airport_id=@p0;@p0 = 1

NHibernate: SELECT zones0_.Terminal_id as Terminal4_1_, zones0_.Id as Id1_, zones0_.Id as Id51_0_, zones0_.Name as Name51_0_, zones0_.IsDeleted as IsDeleted51_0_, zones0_.Terminal_id as Terminal4_51_0_ FROM dbo.[Zone] zones0_ WHERE zones0_.Terminal_id=@p0;@p0 = 2


second select
Are those the same airport instance? False

now adding a terminal
NHibernate: select next_hi from dbo._uniqueKey with (updlock, rowlock)
NHibernate: update dbo._uniqueKey set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16

NHibernate: INSERT INTO dbo.[Terminal] (Name, IsDeleted, Airport_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'terminal added to second airport', @p1 = False, @p2 = NULL, @p3 = 16
NHibernate: UPDATE dbo.[Airport] SET Name = @p0, IsDeleted = @p1 WHERE Id = @p2;@p0 = 'test airport', @p1 = False, @p2 = 1

NHibernate: UPDATE dbo.[Terminal] SET Name = @p0, IsDeleted = @p1, Airport_id = @p2 WHERE Id = @p3;@p0 = 'test terminal', @p1 = False, @p2 = 1, @p3 = 2

NHibernate: UPDATE dbo.[Zone] SET Name = @p0, IsDeleted = @p1, Terminal_id = @p2 WHERE Id = @p3;@p0 = 'test zone', @p1 = False, @p2 = 2, @p3 = 3

NHibernate: UPDATE dbo.[Terminal] SET Airport_id = @p0 WHERE Id = @p1;@p0 = 1, @p1 = 16



my problems are:
1. the strange update behaviour which updates everything...
2. the fact that firstAirport and secondAirport are not the same object (maybe I'm missing something about 2nd level cache?)

thanks in advance,
Jhonny


Solution

  • The fact that firstAirport and secondAirport are not the same object, is due to the fact that reference types are compared for reference-equality by default.

    Since you're loading first and secondAirport using separate NHibernate ISession's, the cache is not being used, since session1 doesn't know anything about session2 and vice versa.
    The 'Identity' pattern that is implemented by NHibernate's Session, is scoped to that session offcourse.

    You can override this behaviour by overriding the Equals and GetHashcode methods properly. You can override the Equals method, so that equality is determined based on the 'Id' of an airport for instance.

    The strange update behaviour is due to the fact that you're updating the object in another session then the session from which it was retrieved. You should see the ISEssion as a UnitOfWork. So, it is best to load the object and save the object in the same session, instead of doing these operations each in its own session. (You can solve this as well by 'locking' the existing airport object into the session that you're using to perform the update as well).

    using (ISession session = this.SessionFactory.OpenSession())
            {
                using (ITransaction transaction = session.BeginTransaction())
                {
    
                    session.Lock (secondAirport, LockMode.None);
    
                    secondAirport.Terminals.Add(new Terminal() { Name = "terminal added to second airport", Zones = new List<Zone>() });
                    session.Update(secondAirport);
                    transaction.Commit();
                }
            }