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

2

7

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.
Mastrianni answered 2/8, 2011 at 14:50 Comment(0)
M
1

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.

Mastrianni answered 8/8, 2011 at 12:56 Comment(0)
W
0

Please see my answer for a similar question, "Cannot assign property value in entity's constructor" . I won't go into all the detail here because you can read it there.

The short answer is that you're accessing a virtual property in your constructor, which is a no-no. The following change should fix the problem. Replace these lines...

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

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

... with these lines:

private IList<ChildTestEntity> _children;

public virtual IList<ChildTestEntity> Children
{
    get { return _children; }
    protected set { _children = value; }
}

public ParentTestEntity()
{
    _children = new List<ChildTestEntity>();
}

FxCop and ReSharper are two different tools that can automatically identify this problem.

Waddle answered 4/8, 2011 at 17:5 Comment(1)
True, that's a bad design oversight in the model (I knew about the rule, but didn't think of it at the time, thanks for reminding me). This isn't linked to the problem however; the test fails in the exact same way with your changes. It seems more linked with the getter of Count as shown by the stack trace.Mastrianni

© 2022 - 2024 — McMap. All rights reserved.