Skip to main content

What is an Active Record anyway?

While looking for Castle ActiveRecord examples, I found a blog by David Hayden - post 1 (design pattern overview), post 2 (some details on a possible implementation) and post 3 (using the Castle Project). David had a series of articles about Rocky Lhotka's .NET Business Objects (one for each chapter), which I have also read and tried to apply. Don't get me wrong, Rocky's CSLA library works, but as of .Net 1.1 it required a great deal of code on my part. There are reasons why I and many others were investigating code generation tools.

I have been studying Castle's ActiveRecord framework for a week and a half, and it is awesome for new development and clean data. During this discussion, remember that ActiveRecord mostly generates the XML mapping files for you and delegates the heavy lifting to NHibernate. If you have a legacy database with "sometimes there" keys, the framework will punish you severely. Below is an example of a legacy scenario that will result in many extra futile selects, attempting to get data that just isn't there.


CREATE TABLE Users (
ID int identity(1,1) not null primary key,
Username varchar(30) not null unique,
Password varchar(30) not null,
CreatedOn smalldatetime not null default getdate()
) ;

CREATE TABLE LogEntries (
UName varchar(30) null,
UEvent varchar(255) null
) ;

INSERT INTO Users(Username, Password) VALUES('admin' , '123') ;
INSERT INTO Users(Username, Password) VALUES('jsmith', '999') ;

INSERT INTO LogEntries (UName, UDate, UEvent)
VALUES('jsmith', '2007-02-21 07:00', 'Woke up') ;
INSERT INTO LogEntries (UName, UDate, UEvent)
VALUES('tjones', '2007-02-21 07:30', 'Looked out kitchen window') ;
INSERT INTO LogEntries (UName, UDate, UEvent)
VALUES('jsmith', '2007-02-21 07:32', 'Ate breakfast') ;
INSERT INTO LogEntries (UName, UDate, UEvent)
VALUES('tjones', '2007-02-21 08:00', 'Drove to work') ;
INSERT INTO LogEntries (UName, UDate, UEvent)
VALUES('khaus' , '2007-02-21 12:00', 'Ate Lunch') ;

You may have noticed that the LogEntries table does not have a foreign key relation to Users. This is because the table was imported from a text file, and we can't guarantee that all users listed are in our database. For this scenario, NHibernate will do an "N+1" fetch if we try to link LogEntries to users. The HQL statement [SELECT le, us FROM LogEntry le LEFT JOIN le.user] will cause us problems no matter what we try. If we add a [BelongsTo] User property in the LogEntry class (NHibernate <many-to-one\> mapping), we find that the PropertyKey xml attribute is not mapped into BelongsTo. If we add the property and modify ActiveRecord's XmlGenerationVisitor.cs to add PropertyRef, we find that NHibernate's SessionImpl.cs and Loader.cs basically ignore the username link because it is not a primary key, and select the associated User once for each log entry. This happens whether the user was loaded in the initial join or not, and whether the user has been loaded into memory or not. I tried lying and telling ActiveRecord that UserName was the primary key. That helped, and I now only saw extra selects when the UserName was not found in the initial join. I patched NHibernate's SessionImpl.cs to remember failed fetches, and reduced the number of selects to 1 + count(unique missing usernames). I can not submit this patch to NHibernate, because it only works for the scenario of joining on a primary key - it breaks for the PropertyRef scenario. The only way that I found to fix this multi-select problem was to add a foreign key reference to LogEntries (it does not have to have a constraint). So LogEntries would look like:


CREATE TABLE LogEntries (
UName varchar(30) null,
UEvent varchar(255) null,
User_ID int null
) ;


Now we have to run an update every time we import a log file, but the subsequent processes will run with good performance (Any remaining performance issues can be solved with indexes, etc.). Do NOT add a foreign key constraint, or your imports will fail on bad data.


UPDATE LogEntries SET User_ID = u.ID
FROM LogEntries le
JOIN Users u ON u.UserName = le.UName
WHERE User_ID is NULL ;


In the future, I will share my log4net file.

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 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

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