posted on Friday, December 10, 2004 7:30 AM
by
mlorengo
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.