Wednesday, April 11, 2007

Defensive Programming with ActiveRecords

[Update 1: finished the entry, and added some compilable code.]
[Update 2: Updated the compilable code - improved the Property setter code.]

Recently, I have been working on a project with Castle's ActiveRecord support. Since I am changing my database schema as I discover new requirements, and adapt to new data sources, I occasionally run into query / model / table mismatches. Now, gross mismatches cause NHibernate to choke, and refuse to accept my models, but if I have merely added a field, that will be ignored until insert time. Even then, it might not cause an exception if it is a nullable field.

Now when I first started on this project, I wrote a simple SQL procedure that selected a list of fields from the database schema, walked through a cursor for each field and PRINTed code, then I would create a class file in Visual Studio 2005 and paste that code in.

-- Get a list of tables
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME != 'sysdiagrams';

SELECT c.Column_Name, c.Column_Default, c.Is_Nullable,
c.Data_Type, c.Character_Maximum_Length,
c.Numeric_Precision, c.Numeric_Scale
FROM Information_Schema.Columns c
WHERE Table_Name = @Table
ORDER BY Ordinal_Position ;


As development has progressed, I have learned to take advantage of the partial class support in .Net 2.0. My current model consists of a partial class with several sections: static fieldname strings, private variables, public constructors - empty and full field list, properties, and optionally a property changed event. Then, I have another (classname)_biz.cs file containing any other methods that I need to solve a problem. The one method that I always have is a ToString over-ride, to make it easy to pop an array of my objects into a list box or combo box. This week, I wrote a simple c# 2.0 windows form application to generate both the model and biz partial classes. Since I have no idea what ToString() should return, I concatenate all non-key fields with braces around them and add a TODO: comment. I use Subversion to protect my code, or more accurately, I use TortoiseSVN - a windows explorer extension that makes commits very fast, and remembers check-in messages. I don't care what YOU use, just use something! And do not put your repository on the same physical disk drive as your development code. Ideally, the repository would be on a remote server, but at the very least put it on another disk drive.



And just to make sure I don't miss something, I wrote a Unit test that walks through my list of ActiveRecord entities and verifies that private variables, properties and static strings Prop_FieldName = "FieldName" all meet expectations. Unmapped fields are listed in the debug output, but not considered errors. Field name mismatches cause an error.

Now, you might ask why I would bother with the "static string Prop_FieldName" section. This centralizes my field name references when using NHibernate's Criteria or in my case either an ICriterion array or a DetachedCriteria. Below is an example SQL table, model class and (finally) some business logic to load a record by a service code. In this line "Expression.Eq(ServiceType.Prop_TypeCode, p_TypeCode)", Prop_TypeCode is guaranteed by my unit tester to match the like-named field and corresponding database property. This protects me from spellings errors and from inadvertently referencing a field that is not part of my table. Now, I did not go so far as to decompile the Code DOM and ensure that each property references the correct private variable. I have to start trusting myself somewhere! As a point of interest, If I were to significantly re-structure my database, to the point that NHibernate would not register my model classes (entities), all I have to do is generate the model classes into another directory and do a "diff". Once the models compile and pass their tests again, the business code should also work as expected. (As a side note, This is not an example of how I format my code, but Blogger's layouts work better if I have shorter lines.)


// SQL table definition
CREATE TABLE ServiceTypes (
ID smallint identity(1,1) primary key not null,
TypeName varchar(30) not null,
TypeCode char(3) not null
)

// ServiceType.cs
namespace Demo.Model
{
// Generate class for table ServiceType
// value class ServiceType generated from ServiceTypes
// RTate [04/12/2007] Created

using System;
using System.ComponentModel;
using Castle.ActiveRecord;

[ActiveRecord("ServiceTypes")]
public partial class ServiceType
: ActiveRecordValidationBase, INotifyPropertyChanged
{

#region Property_Names

public static string Prop_ID = "ID";
public static string Prop_TypeName = "TypeName";
public static string Prop_TypeCode = "TypeCode";

#endregion

#region Private_Variables

private short _id;
private string _TypeName;
private string _TypeCode;

#endregion

#region Constructors

public ServiceType()
{
}

public ServiceType(
short p_id,
string p_TypeName,
string p_TypeCode)
{
_id = p_id;
_TypeName = p_TypeName;
_TypeCode = p_TypeCode;
}

#endregion

#region Properties

[PrimaryKey("ID", Access=PropertyAccess.NosetterLowercaseUnderscore)]
public short ID
{
get { return _id; }
}

[Property("TypeName", NotNull=true, Length=30), ValidateLength(1,30)]
public string TypeName
{
get { return _TypeName; }
set
{
if ((_TypeName == null) || (!value.Equals(_TypeName)))
{
_TypeName = value;
NotifyPropertyChanged("TypeName");
}
}
}

[Property("TypeCode", NotNull=true, Length=3), ValidateLength(1,3)]
public string TypeCode
{
get { return _TypeCode; }
set
{
if ((_TypeCode == null) || (!value.Equals(_TypeCode)))
{
_TypeCode = value;
NotifyPropertyChanged("TypeCode");
}
}
}

#endregion

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}

#endregion

} // ServiceType
}

// ServiceType_biz.cs
namespace Demo.Model
{
// business partial class ServiceType
// generated from table ServiceTypes
// RTate [04/12/2007] Created

using System;
using NHibernate.Expression;

public partial class ServiceType
{

#region Business Methods

public ServiceType FindByTypeCode(string p_TypeCode)
{
ICriterion[] crit = new ICriterion[1];
crit[0] = Expression.Eq(ServiceType.Prop_TypeCode, p_TypeCode);
return ServiceType.FindFirst(crit, null);
}

public override string ToString()
{
//TODO: Choose a suitable string representation
return "[" + TypeName + "]" + " [" + TypeCode + "]";
}

#endregion

} // ServiceType
}

Labels: , , ,

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

Links to this post:

Create a Link

<< Home