The conception, birth, and first steps of an application named Charlie

Subscribe: Atom or RSS

The Valley of Data Access - Part 3

by Alister Jones (SomeNewKid)

At this point in the life of Charlie, I am refactoring the data access code. In the previous weblog entry, I explained why my approach is to extract from the data access methods the parts that stay the same, and leave the mappers to concentrate on their unique requirements. Staying with the earlier example of a Retrieve method, I see four unique parts that the Mapper must gather together before passing them off to the Helper to execute.

Starting from the top of the method and working down, the first unique part is the entity that is being retrieved.

Document document = (Document)entity;

The second unique part of the Retrieve method is the query string:

String query =
   @"SELECT
         d.Document_Id,
         d.Document_ParentId,
         d.Document_Name,
         d.Document_FriendlyUrl,
         d.Document_Position,
         d.Document_CreationDate, 
         d.Document_UpdateDate, 
         c.DocumentLocalized_Culture,
         c.DocumentLocalized_Title
     FROM
            Charlie_Document d
         JOIN
            Charlie_DocumentLocalized c
         ON
            c.Document_Id = d.Document_Id
     WHERE ";
if (criteria.LoadById == true)
{
   query += " d.Document_Id = @documentid";
}
else if (criteria.LoadByUrl == true)
{
   query += " d.Document_FriendlyUrl = @friendlyurl";
}

Now, a seasoned developer would be horrified at the appearance of a hard-coded query like that. Personally though, I consider its clumsiness to be offset by three compelling benefits. First, it communicates clearly the query that will be executed. Second, it allows me to copy the query into Query Analyser, test it and perhaps tweak it, and paste it back into Charlie’s code. Third, it’s simple. The seasoned developer can add attributes to the business objects or add mapping rules to an XML file. Charlie and I will just slap the query in place.

The third unique thing that the Retrieve method must gather together is the collection of command parameters. Currently, the code looks like this:

if (criteria.LoadById == true)
{
   command.Parameters.AddWithValue("@documentid", criteria.Id);
}
else if (criteria.LoadByUrl == true)
{
   command.Parameters.AddWithValue("@friendlyurl", criteria.Url);
}

This just needs to be updated slightly so that the mapper gathers together a single collection of parameters:

ParameterCollection parameters = new ParameterCollection();

if (criteria.LoadById == true)
{
   parameters.Add(new Parameter("@documentid", criteria.Id));
}
else if (criteria.LoadByUrl == true)
{
   parameters.Add(new Parameter("@friendlyurl", criteria.Url));
}

I will use a custom Parameter class, because I want to be able to define “alternative” values in order to support the cascading logic used throughout Charlie. Using localization as an example, the following may be a custom parameter:

Parameter cultureParameter = new Parameter("@culture", "fr-FR", "fr", "en");

The final unique thing within each mapper’s Retrieve method is seen in the bold line below:

if (reader.Read())
{
   document = NewDocumentFromReader(reader);
}

The document was the first unique thing gathered by the Mapper, so we have that part. But what can we do about the second part? How can we tell the Helper method that once it has executed the passed-in query and obtained the resulting data reader, we want that reader to be passed to our NewDocumentFromReader method?

What would seem to be an obvious solution would be to have the Helper simply return the reader to the calling Mapper code. The Mapper code then passes the reader to the NewDocumentFromReader method. However, the Mapper is then left holding a darn data reader, which it must tidy up. But the whole point of this refactoring exercise is to free the Mapper from having to worry about data connections, transactions, readers, exceptions, and everything else.

Fortunately there is another solution. Just as the Mapper can pass the entity object, the query string, and the parameter collection to the Helper class, it can also pass an entire method to the Helper class. In truth, it does not pass the method itself but rather a delegate of the method. Teemu Keiski describes this process in his article, Using Delegates with Data Readers to Control DAL Responsibility. If the concept of delegates is new for you, I wrote a tiny tutorial on delegates on the ASP.NET Forums.

We have seen that the Retrieve method in each EntityMapper needs to gather together four objects to send to its Helper class to execute. The skeleton of the Retrieve class therefore looks like this:

public override Entity Retrieve(Entity entity, EntityCriteria crit)
{
    // Get the entity
    Document document = (Document)entity;
    
    // Get the query
    String query = @"SELECT ... ";
    
    // Get the parameters
    ParameterCollection parameters = new ParameterCollection();
    parameters.Add(new Parameter("@name", value));
    
    // Get the delegate
    IReaderHandler handler = new IReaderHandler(NewDocumentFromReader)); 
    
    // Pass them off to the Helper for executing
    this.Helper.Retrieve(document, query, parameters, handler);

    return document;
}

This trim Mapper.Retrieve method means that all the red code in the previous weblog entry has been extracted to the Helper.Retrieve method. This Helper.Retrieve method assumes responsibility for the database connection, any transactions that may be in progress, any readers that are generated, any exceptions that are thown, and any logging that may be required. Since all the Mappers will use this Helper.Retrieve method, there is now a single point at which the data access code can be tweaked or corrected.

Now, as I said in the first entry on refactoring the data access code, I am writing this series of weblog entries “live”—I have not yet performed this refactoring.

The next step is to actually do the refactoring. I will of course report on any surprises along the way.

by Alister Jones | Next up: The Valley of Data Access - Part 4

0 comments

----