Skip to main content

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
}

Comments

Popular posts from this blog

Castle ActiveRecord with DetachedCriteria

My current development environment is Visual Studio Express C# Edition (read that as free ), Castle ActiveRecord's latest svn trunk(usually within a few days), and NHibernate svn trunk. As of NHibernate version 1.2.0, there is a very cool new class out there ... DetachedCriteria. This class lets you set all of your Castle relational attributes like BelongsTo, HasMany, etc. as lazy fetch, and over-ride this for searches, reports, or anytime you know ahead of time that you will be touching the related classes by calling detachedCriteria.SetFetchMode(..., FetchEnum.Eager). As a good netizen, I have tried to contribute to NHibernate and Castle ActiveRecord even if only in the smallest of ways . Oh yeah, I tried mapping to a SQL VIEW, and it worked GREAT! I received a comment after my last post, indicating that there is a better way, and I am sure of it, but the view guaranteed that I only have one database request for my dataset. NHibernate was wanting to re-fetch my missing as

Castle ActiveRecord with Criteria and Alias

Update May 25, 2007: ActiveRecord now supports DetachedCriteria, which eliminates the need for the SlicedFindAll that I wrote below. It is nice when a library moves to add support for such commonly needed functions. So in summary, use Detached criteria instead of the code below. It is still a nice example of using NHibernate sessions. I have a history log, where each history record "belongs to" a service record. I have to treat this as a child-to-parent join, since some children are orphans. I wanted to use the FindAll(Criteria), but I wanted the option to have optional criteria, orders and aliases. My solution was to create an ARAlias class to represent an Associated Entity and an alias, and then build an ARBusinessBase class with the following method: public static T[] SlicedFindAll(int firstResult, int maxResults, Order[] orders, ARAlias[] aliases, params ICriterion[] criteria) { IList list = null; ISessionFactoryHolder holder = ActiveRecordMediator.GetSessionF

Castle ActiveRecord calling a Stored Procedure

Update: I have contributed patch AR-156 that allows full integration of Insert, Update and Delete to ActiveRecord models . If you've been reading my blog lately, you know that I have been seriously testing the Castle ActiveRecord framework out. I really love it, but I have an existing Microsoft SQL Server database with many stored procedures in it. I have tested the ActiveRecord model out, and I am sure that I will learn enough to be able to use it for standard CRUD (create, read, update, delete aka. insert, select, update, delete) functionality. BUT ... If I really want to integrate with my existing billing procedures, etc, I will have to be able to call stored procedures. I have taken two approaches ... write the ARHelper.ExecuteNonQuery(targetType, dmlString) method that gets a connection for the supplied type, executes dmlString, and closes it. write the ARHelper.RegisterCustomMapping(targetType, xmlString) method that allows me to add mappings that refer to my auto-gener