Friday, December 10, 2004 - Posts

Novocain for the Soul (Time to Think)

So, I know I was supposed to talk about my user stories / use cases for the Producer/Wine model of the virtual cellar, but I decided to delay that a little bit to talk about a few things that have been going on. I went to the dentist today to get my teeth cleaned and it just so happens that the weather in Seattle cleared up (stopped raining) long enough for me to ride my bike in from Bellevue to my dentist in Seattle. Usually I listen to the radio when I ride my bike (in one ear only), but this time I forgot to pack my cell phone, so I had no one to bother me except myself. And during my ride, and the time in the dentist chair I began to think...

The Master Detail Relationship

There's been a little bit of code that's been bothering me in the VirtualCellar, and I'm not sure what I'm going to do about it. If anybody has any ideas, please pass them along. If we look at the UML diagram from yesterday, you'll see that the Wine class has a Varietals property that is a collection WineVarietal classes. Indeed I have a WineVarietalCollection class that derives from System.Collections.CollectionBase to handle the classes in a strongly typed manner. The thing that is bothering me is how to handle the persisting of the classes (and underlying records). Here's a simple illustration of what I'm talking about.

We have a class Master that has an attribute Details, which is a collection of Detail classes for the instance of Master with a particular .Id attribute

class Master
{
	public int Id
	{
		get {return _id;}
	}

	public string Name
	{
		get {return _name;}
		set {_name = value;}
	}

	public DetailCollection Details
	{
		get {return _details;}
	}

	private string _name;
	private DetailCollection _details;
}

class DetailCollection : System.Collections.CollectionBase
{
	...CollectionBase Implementation
}

class Detail
{
	public Detail(int amount)
	{
		_amount = amount;
	}
	
	public Detail(IDataReader dr)
	{
		// Simplified, should really check for null
		_amount = dr["Amount"];
	}
	
	public int Amount
	{
		get {return _amount;}
		set {_amount = value;}
	}

	private int _amount;
}

To help in the creation of a Master class instance, I create a MasterFactory class that has the following definition:
 

class MasterFactory
{
	public static Master Create(IDataReader dr)
	{
		Master m = new Master();

		// Simplified, should really check for null
		m.Id = dr["Id"];		
		m.Name = dr["Name"];

		// Load the Details attribute with the matching Detail classes		
		m.Details = new CreateDetailCollection(m.Id);
		
		return m;
	}
	
	// Creates a DetailCollection for the given Master Id
	private DetailCollection CreateDetailCollection(int masterId)
	{
		DetailCollection dc = new DetailCollection();
		
		// Not shown, but GetDetailDataProvider returns a dataprovider class
		// that implements the IDetailDataProvider interface (methods to retrieve information
		// from the Detail dataset) for the database configured in the .config file
		IDetailDataProvider ddp = new GetDetailDataProvider();
		
		// Get the Detail records for the specified Master record
		IDataReader dr = ddp.GetByMasterAsDataReader(masterId);

		// Exception handling purposely left out for clarity
		while(dr.Read())
		{
			// Add a new Detail class to the DetailCollection
			dc.Add(new Detail(dr));
		}		
		
		return dc;
	}	
}		

I also toyed with the idea of having the DetailCollection class have a constructor that looked like this

public DetailCollection(int masterId)
{
	// Put CreateDetailCollection(int masterId) implementation here
}

However I decided against this because the DetailCollection (or WineVarietalCollection) class is really a part of the Master class and the responsibility for it's creation should be handled in the MasterFactory class. Opinions on this anybody?

Now, to get to the real meat of the problem. How to handle changes to items in the Detail collection, either modifications of existing Detail items, or the addition or removal of Detail items in the collection?

To address this I'll need to introduce the MasterRepository class.

public MasterRepository : IRepository
{
	public RepositoryItem Load(int id)
	{
		return MasterFactory.Create(id);
	}
	
	public Save(RepositoryItem ri)
	{
		Master m = ri as Master;
		
		switch(ri.ItemState)
		{
			case RepositoryItemState.New
			{
				ri.Id = GetMasterDataProvider().Add(m.Name);
				AddDetailCollection(m.Details);
				ri.ItemState = RepositoryItemState.Clean;
				break;
			}
			
			case RepositoryItemState.Dirty
			{
				GetMasterDataProvider().Update(m.Id, m.Name);
				UpdateDetailCollection(m.Id, m.Details);
				ri.ItemState = RepositoryItemState.Clean;
			}
		}

		// Add a Detail record for each class in the collection		
		private void AddDetailCollection(int masterId, DetailCollection dc)
		{
			IDetailDataProvider ddp = GetDetailDataProvider();
			
			// Add back all the Detail records in the passed DetailCollection
			foreach(Detail d in dc)
			{
				ddp.Add(masterId, d.Amount);
			}
		}
			
		// Update the Detail records by deleting existing records and then
		// adding a Detail record for each class in the collection
		private void UpdateDetailCollection(int masterId, DetailCollection dc)
		{
			IDetailDataProvider ddp = GetDetailDataProvider();
			
			// Delete all the records in the Detail table for the given master
			ddp.DeleteAllByMaster(masterId);
			
			// Add the collection back
			AddDetailCollection(masterId, dc);
		}
	}
		

So now I can do the following to Load an existing Master instance, modify the Detail records and then save the changes...

Master m = MasterRepository.Load(1);
m.Details.Add(new Detail(21));
MasterRepository.Save(m);

Of course, I've left out all of the transaction code to make sure that all the updates happen together, but I wanted to keep the problem and solution simple. I also toyed with the idea of having a public DataSet GetMasterAsDataSet(int id) method, and return a DataSet with two Tables (Master, Detail) and a DataRelation setup between them. Then I could make the Master class encapsulate the DataSet and adapt Master and Detail operations to DataSet methods. Any opinions on that approach?

Incidentally, David Hayden, discusses Class responsibilities with respect to contained aggregates on his blog.

with 2 Comments