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

Updated ActiveRecord Code Generator

Today, I updated the ActiveRecord Code Generator a bit. I checked in changes to use primary and foreign key details from INFORMATION_SCHEMA. The original code used naming conventions to decide what various fields were used for - ID = Primary Key, Field_ID = Foreign Key to table Fields. If you want to use naming conventions, let me know and I can add a setting in App.Config to allow this (along with any "real" key constraints).

How does Rails scaffolding select HTML input tags?

Recently, a reader saw my fix for SQL Server booleans, and asked me a followup question: why does Rails display a yes/no selection instead of a checkbox? The short answer is look in {RUBY_HOME} /lib/ruby/gems/1.8 /gems/actionpack-1.10.2 /lib/action_view/helpers, but your path may vary depending on whether you are using gem, "edge rails", etc. Anyway, look in the file "active_record_helper.rb" for a method called "all_input_tags", and notice that it calls "default_input_block" if you don't supply an input_block. Now notice that "default_input_block" creates a label and calls "input(record, column.name)" which in turn calls "InstanceTag#to_tag" which finally looks at the datatype and maps boolean to a select tag. Perhaps a wiser Rails explorer can provide us with the rationale for this, but I guess we could add a MixIn for InstanceTag that redefines the to_tag() method, or just do a dirty and unmaintainable hack l...

Features of the Code Generator

I just updated my code generator to optionally generate validation attributes. This simple change includes App.config file entries for all check boxes, and a new checkbox for "Validation" - aka validation attibute generation. While I was making this change, I realized that I really need to pass a CodeGenerationContext object to the DbTable, DbField and ModelGenerator classes. The requester can populate the context, and pass it to the code generator. Anyway, enough about the code, let's talk about the templates. I made a simple template this weekend to generate a DataGridView column array, suitable for databinding. I'm sure my new template will need some tweaks to handle Foreign Keys better (it currently just displays them as TextBox). Let's look at a template. ##FILENAME:PR_${table.GetClassName()}_Insert.sql ## ## Generate a SQL stored procedure to insert a record into the ## specified table and return the newly created primary key ## ## FUTURE: The generat...