Tuesday, August 9, 2011

Don't initialize collection on your POCO/POJO constructor, it's dangerous on session.Merge

If you received this kind of error in NHibernate..

NHibernate.HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: TestProject.SampleModel.Product.PriceList


.., chances are you are using session.Merge on your code, an example code:


public void Save(Ent ent, byte[] version)
{
	try
	{
		using (var tx = _session.BeginTransaction())
		{
			object pkValue =
				typeof(Ent).InvokeMember(
					PrimaryKeyName,
					System.Reflection.BindingFlags.GetProperty, null, ent, new object[] { });

			
			_session.Evict(_session.Load<Ent>(pkValue));


			Type entType = typeof(Ent);
			entType.InvokeMember(VersionName, System.Reflection.BindingFlags.SetProperty, null, ent, new object[] { version });
			
			Ent retObject = (Ent) _session.Merge(ent);                                        
			
			tx.Commit();
						
			object pkGenerated = typeof(Ent).InvokeMember(PrimaryKeyName, System.Reflection.BindingFlags.GetProperty, null, retObject, new object[]{});
			typeof(Ent).InvokeMember(PrimaryKeyName, System.Reflection.BindingFlags.SetProperty, null, ent, new object[] {  pkGenerated });

			object retRowVersion = typeof(Ent).InvokeMember(VersionName, System.Reflection.BindingFlags.GetProperty, null, retObject, new object[] { });
			typeof(Ent).InvokeMember(VersionName, System.Reflection.BindingFlags.SetProperty, null, ent, new object[] { retRowVersion });
			
			
		}
	}
	catch (StaleObjectStateException)
	{
		throw new DbChangesConcurrencyException();
	}
}



[TestMethod]
public void Common_CanUpdate(IRepository<Product> db)
{
	// Arrange            	
	var px = new Product { ProductName = "Bumble Bee", Category = "Autobots", MinimumPrice = 8 };
	db.Save(px, null);
	
	var fromWeb = 
		new Product 
		{ 
			ProductId = px.ProductId, 
			ProductName = px.ProductName, 
			Category = px.Category, 
			MinimumPrice = px.MinimumPrice, 
			RowVersion = px.RowVersion 
		};

	// Act
	string expecting = "Bumble Bee Battle Mode";
	fromWeb.ProductName = expecting;         
	db.Save(fromWeb, fromWeb.RowVersion); // this line causes the NHibernate.HibernateException above
	
	
	// Assert            
	Assert.AreEqual(expecting, db.Get(fromWeb.ProductId).ProductName);
	Assert.AreNotEqual(px.ProductName, db.Get(fromWeb.ProductId).ProductName);	
}

This produces an exception on session.Merge:
var fromWeb = 
	new Product 
	{ 
		ProductId = px.ProductId, 
		ProductName = px.ProductName, 
		Category = px.Category, 
		MinimumPrice = px.MinimumPrice, 
		RowVersion = px.RowVersion

		// Collection initializer is missing here
	};

NHibernate.HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: TestProject.SampleModel.Product.PriceList

To fix that exception, you must explicitly set your collections, then the exception will be gone.

Depending on your program requirements, you may either do this:

var fromWeb = 
	new Product 
	{ 
		ProductId = px.ProductId, 
		ProductName = px.ProductName, 
		Category = px.Category, 
		MinimumPrice = px.MinimumPrice, 
		RowVersion = px.RowVersion,
		
		PriceList = new List<ProductPrice>()		
	};	

Or this:

var fromWeb = 
	new Product 
	{ 
		ProductId = px.ProductId, 
		ProductName = px.ProductName, 
		Category = px.Category, 
		MinimumPrice = px.MinimumPrice, 
		RowVersion = px.RowVersion,
		
		PriceList = px.PriceList
	};	



Though you might be tempted to initialize collections on your constructor, it's a dangerous thing to do so when using session.Merge..

public class Product
{
	public virtual int ProductId { get; set; }
	public virtual string ProductName { get; set; }
	public virtual string Category { get; set; }        
	public virtual decimal MinimumPrice { get; set; }

	public virtual IList<ProductPrice> PriceList { get; set; }
	
	public virtual byte[] RowVersion { get; set; }
	
	public Product()
	{
		PriceList = new List<ProductPrice>();
	}
}



..,the corresponding database rows of children entities will be emptied by NHibernate when you forgot to populate the List items

[TestMethod]
public void Common_CanUpdate(IRepository<Product> db)
{


    // Arrange                
    var px = new Product
        {
            ProductName = "Bumble Bee",
            Category = "Autobots",
            MinimumPrice = 8
        };
    px.PriceList =
        new List<ProductPrice>()
        {
            new ProductPrice { Product = px, Price = 234, EffectiveDate = DateTime.Today },
            new ProductPrice { Product = px, Price = 300, EffectiveDate = DateTime.Today.AddDays(100) }
        };    


    
        db.Save(px, null);
    
    var fromWeb = 
        new Product 
        { 
            ProductId = px.ProductId, 
            ProductName = px.ProductName, 
            Category = px.Category, 
            MinimumPrice = px.MinimumPrice, 
            RowVersion = px.RowVersion 
            
            // PriceList collection was not set here. However, it was automatically inititialized in constructor
        };

    // Act
    string expecting = "Bumble Bee Battle Mode";
    fromWeb.ProductName = expecting;         
    db.Save(fromWeb, fromWeb.RowVersion); 
    
    // session.Merge won't have any problem if the PriceList is not null. 
    // However, NHibernate will empty the corresponding database
    // rows of your children entities if the List has no items. Really dangerous
        
    
    
    // Assert            
    Assert.AreEqual(expecting, db.Get(fromWeb.ProductId).ProductName);
    Assert.AreNotEqual(px.ProductName, db.Get(fromWeb.ProductId).ProductName);    
}

To err on the safe side, don't initialize any collections on your POCO's constructor. Let the exception be an indicator of code smell in your program, you cannot make the exception go away by merely suppressing it; and sometimes it introduces bigger problem if you do so.

No comments:

Post a Comment