Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does NHibernate throw a "GenericADOException : could not initialize a collection" exception during ISession.Refresh in this case?

I've been been experiencing a weird behaviour (at least to me) with ISession.Refresh().

I've got an entity with a lazy-loaded collection of children, and a read-only property that hits this collection, all included inside the 2nd-level cache. I use ISession.Refresh() in a long session to get the latest data after committing a transaction to DB, and get the following error:

NHibernate.Exceptions.GenericADOException : could not initialize a collection: [Test.NUnit.DBTest.TestModel.ParentTestEntity.Children#d4251363-cf88-4684-b65a-9f330107afcf][SQL: SELECT children0_.ParentTestEntity_id as ParentTe4_1_, children0_.Id as Id1_, children0_.Id as Id42_0_, children0_.RowVersion as RowVersion42_0_, children0_.Parent_id as Parent3_42_0_ FROM "ChildTestEntity" children0_ WHERE children0_.ParentTestEntity_id=?]
  ----> System.NullReferenceException : Object reference not set to an instance of an object.
    at NHibernate.Loader.Loader.LoadCollection(ISessionImplementor session, Object id, IType type)
    at NHibernate.Loader.Collection.CollectionLoader.Initialize(Object id, ISessionImplementor session)
    at NHibernate.Persister.Collection.AbstractCollectionPersister.Initialize(Object key, ISessionImplementor session)
    at NHibernate.Event.Default.DefaultInitializeCollectionEventListener.OnInitializeCollection(InitializeCollectionEvent event)
    at NHibernate.Impl.SessionImpl.InitializeCollection(IPersistentCollection collection, Boolean writing)
    at NHibernate.Collection.AbstractPersistentCollection.Initialize(Boolean writing)
    at NHibernate.Collection.AbstractPersistentCollection.ReadSize()
    at NHibernate.Collection.PersistentBag.get_Count()
    DBTest\TestModel\EntiteTestCacheCollectionsParent.cs(25,0): at Test.NUnit.DBTest.TestModel.ParentTestEntity.get_Count()
    at (Object , GetterCallback )
    at NHibernate.Bytecode.Lightweight.AccessOptimizer.GetPropertyValues(Object target)
    at NHibernate.Tuple.Entity.PocoEntityTuplizer.GetPropertyValuesWithOptimizer(Object entity)
    at NHibernate.Tuple.Entity.PocoEntityTuplizer.GetPropertyValues(Object entity)
    at NHibernate.Persister.Entity.AbstractEntityPersister.GetPropertyValues(Object obj, EntityMode entityMode)
    at NHibernate.Event.Default.AbstractVisitor.Process(Object obj, IEntityPersister persister)
    at NHibernate.Event.Default.DefaultRefreshEventListener.OnRefresh(RefreshEvent event, IDictionary refreshedAlready)
    at NHibernate.Event.Default.DefaultRefreshEventListener.OnRefresh(RefreshEvent event)
    at NHibernate.Impl.SessionImpl.FireRefresh(RefreshEvent refreshEvent)
    at NHibernate.Impl.SessionImpl.Refresh(Object obj)
    DBTest\NHibernateBehaviorTests.cs(610,0): at Test.NUnit.DBTest.NHibernateBehaviorTests.Test()
    --NullReferenceException
    at NHibernate.Engine.Loading.CollectionLoadContext.AddCollectionToCache(LoadingCollectionEntry lce, ICollectionPersister persister)
    at NHibernate.Engine.Loading.CollectionLoadContext.EndLoadingCollection(LoadingCollectionEntry lce, ICollectionPersister persister)
    at NHibernate.Engine.Loading.CollectionLoadContext.EndLoadingCollections(ICollectionPersister persister, IList`1 matchedCollectionEntries)
    at NHibernate.Engine.Loading.CollectionLoadContext.EndLoadingCollections(ICollectionPersister persister)
    at NHibernate.Loader.Loader.EndCollectionLoad(Object resultSetId, ISessionImplementor session, ICollectionPersister collectionPersister)
    at NHibernate.Loader.Loader.InitializeEntitiesAndCollections(IList hydratedObjects, Object resultSetId, ISessionImplementor session, Boolean readOnly)
    at NHibernate.Loader.Loader.DoQuery(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies)
    at NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies)
    at NHibernate.Loader.Loader.LoadCollection(ISessionImplementor session, Object id, IType type)

Here is a unit test that show the problem with a simplified model:

    [Test]
    public void Test()
    {
        ISession session1 = NHibernateHelper.SessionFactory.OpenSession();
        ISession session2 = NHibernateHelper.SessionFactory.OpenSession();

        // Create a new entity tree and persist it
        ParentTestEntity parentSession1 = new ParentTestEntity();
        parentSession1.AddChild(new ChildTestEntity());
        session1.Save(parentSession1);
        session1.Flush();

        // Load the saved object into another session
        ParentTestEntity parentSession2 = session2.Get<ParentTestEntity>(parentSession1.Id);
        session2.Refresh(parentSession2); // Throws here
    }

Here are the entities involved:

public class ParentTestEntity
{
    public virtual Guid Id { get; private set; }
    public virtual long RowVersion { get; private set; }

    public virtual IList<ChildTestEntity> Children { get; protected set; }

    public ParentTestEntity()
    {
        this.Children = new List<ChildTestEntity>();
    }

    public virtual int Count
    {
        get
        {
            return Children.Count;
        }

        set { }
    }

    public virtual void AddChild(ChildTestEntity child)
    {
        if (this.Children == null)
        {
            this.Children = new List<ChildTestEntity>();
        }

        this.Children.Add(child);
        child.Parent = this;
    }
}

public class ChildTestEntity
{
    public virtual Guid Id { get; private set; }
    public virtual long RowVersion { get; private set; }

    public virtual ParentTestEntity Parent { get; set; }
}

And their mappings:

public class ParentTestEntityMap : ClassMap<ParentTestEntity>
{
    public ParentTestEntityMap()
    {
        Cache.ReadWrite();

        Id(x => x.Id)
            .GeneratedBy.GuidComb();

        Version(x => x.RowVersion);

        HasMany(x => x.Children)
            .Inverse()
            .Cascade.All()
            .Cache.ReadWrite();

        Map(x => x.Count);
    }
}

public class ChildTestEntityMap : ClassMap<ChildTestEntity>
{
    public ChildTestEntityMap()
    {
        Cache.ReadWrite();

        Id(x => x.Id)
            .GeneratedBy.GuidComb(); 

        Version(x => x.RowVersion);

        References(x => x.Parent)
            .Not.Nullable();
    }
}

During my tests I found that either:

  • removing the mapping of the Count property,
  • removing the Cache.ReadWrite() mappings,
  • enumerating the collection before Refresh,

are enough for the Refresh to work correctly.

Anybody has any idea what I could do to have Refresh work ?

Notes:

  • I can reproduce this behaviour in both NHibernate 2.1.2 and 3.1.0,
  • I know the empty setter in Count is ugly, it's here only to mirror the mapping of the entity in the real model.
like image 387
Mathieu Garstecki Avatar asked Aug 02 '11 14:08

Mathieu Garstecki


1 Answers

Using Load() in replacement to Get() cirucmvents the problem.

This is not a general solution, as Load() has slightly different semantics, and will throw if the corresponding row doesn't exist.

This is an acceptable constraint in our architecture though, as we know that the entites loaded exist in the DB.

Any solution with Get() is still welcome however.

like image 163
Mathieu Garstecki Avatar answered Oct 21 '22 19:10

Mathieu Garstecki