Castle’s MonoRail/ActiveRecord Tips

Our group at Asynchrony released sothebyshomes.com on May 31st. This was our first publicly available MonoRail web site. Below are the lessons we learned while building sothebyshomes.com.

MonoRail

One Controller per ActiveRecordWe did not adhere to this rule at the beginning of the project. One-to-one mappings allow for simpler and easier to read code.Use the Session as Little as PossibleThis is really not a tip for just MonoRail applications. Any high performance web application should adhere to this rule as much as possible. We found this out the hard way.Load Test Early and OftenAgain, not just a MonoRail tip. We still are not very good at this tip. Do any of you have any recommendations for good load testing tools?Restful UrlsThis is nice to have, but it really makes your application look professional. We are currently using URL Rewriter for .NET. I really wish this functionality was better integrated into MonoRail.Tune IISSpend some time learning how to tune IIS. There are no one size fits all tuning options, but the defaults are not very good for high performance sites.Write Automated User Acceptance Tests from the BeginningWe did not start writing automated User Acceptance Tests (UATs) until about a third of the project was completed. This has come back to bite us on several occasions. We are currently writing our UATs in WatiN.Read my UAT framework review for more information.Testing ControllersDo not use the controller testing technique described by the current MonoRail documentation.Read my Testing MonoRail Controllers post for more information.

ActiveRecord

Audit SQL QueriesWe use SQL Server Profiler to audit our queries. This is a great way to find caching opportunities in your application. Start by counting the number of duplicate queries for one use case. You might be surprised. In one page load, we ran the same query 20+ times 😉Relationships are Unreliable During High LoadUnder high load our application would throw NullReferenceExceptions when accessing relationship collections. I am convinced this is a bug in NHibernate, but I cannot prove it. We eventually did the following to protect ourself.

    [HasMany(typeof (ListingImage),        Access = PropertyAccess.FieldCamelcase,        ColumnKey = "listingId",        Table = "ListingImage",        Where = "Floorplan = 0",        OrderBy = "ImageOrderNo")]    public IList PropertyImages    {        get        {            if (propertyImages == null) return new List<ListingImage>();            return propertyImages;        }    }

Use MARS on SQL Server 2005Use the following on your connection strings; MultipleActiveResultSets=true. MARS lets NHibernate/ActiveRecord perform multiple operations on a single connection even if there are default result sets open on the connection. Without MARS, exceptions will be thrown during high load. If it was not for Brian Button’s Google Kung Fu, we might still be trying to find a race condition in NHibernate/ActiveRecord.

If someone knows the exception that is thrown when MARS is turned off, please let me know I would like to post it.

Unit Tests Should NOT Depend on DataWe just got an updated database from our client with new production quality data. Our model tests depend on the data in our current database. Oops. 150+ failed tests. Day 2 of fixes. Enough said.We really wish Castle’s ActiveRecord had RoR like fixtures. Does anyone have a good solution to this problem?One-to-One Mapping Between Classes and Tables is not Always GoodWe were given a highly normalized legacy database at the start of the project. At the time, we had a one-to-one relationship between classes and tables. The one-to-one mapping made the model very hard to understand.I would encourage you to use indirection to separate your database schema from your domain model. We used database views to help us with this task (our application is read-only). Another approach is to add a thin layer over ActiveRecord, using the repository pattern, to allow your domain model to be separated from your mapping layer.Lazy Should not be AbusedMaking your ActiveRecord classes lazy at the class level is generally not a good idea. When a class is declared lazy the following is true.

    Listing listing = Listing.Find(1);  // 1 is not a valid listing id    // the following throws an exception because NHibernate cannot    // find the listing with an id of one.    listing.ShowPrice();

This means that a finder query only returns the ids of the records. When your code tries to access any part of a found object NHibernate executes another query to fetch the rest of the data for that record. This approach has two problems.

1.  Finders can return objects that are not valid (see example above).2.  Two queries are executed when only one is necessary.

We did not always understand these two things and they caused us problems.Use good Software PracticesWe ran into performance problems by not using good software practices.

    listing.OpenHouses.Count > 1

This is an actual snippet of code from one of our views. This is not necessarily bad code, but it allows little opportunity for optimization. This would have been much better.

    listing.HasOpenHouses

Do you have any tips/comments? I would love to hear them.

Advertisements

13 Responses

  1. > I really wish this functionality was better integrated into MonoRail.

    What exactly do you mean? The UrlBuilder can be configured to use extensions or not (for example).

    > We really wish Castle’s ActiveRecord had RoR like fixtures.

    Not sure if that in XML will look as elegant. Implementing an yaml parser is not an option.

  2. Routing

    I want routing integrated with UrlBuilder. RoR’s link_to allows you to pass in the name of route. Routing feels like an add on to monorail and not an integrated part of the system.

    Fixtures

    What about using IronPython to parse the YAML? There are several Python YAML parsers.

  3. Thanks for the tips!

    One question although, how did you solved:
    “Unit Tests Should NOT Depend on Data”

  4. We haven’t yet. If we find a good way, I will post it.

  5. […] & ActiveRecord tips 14 06 2007 Adam Esterline has posted a bunch of MonoRail & ActiveRecord tips over on his blog. Some of the detail is fairly standard stuff but I learned a few things: […]

  6. Good post.

    Do you drop and recreate your DB schema for testing? Are you creating data specific for the fixture in the setup and teardown methods?
    Another approach is to mock out any interaction with the db for your normal test runs and have a suite of tests for integration which hit the DB. These are usually run on a big check-in or nightly build for instance.

  7. No we do not drop and recreate the DB schema.

    We have created data specific for the fixture in the setup and teardown in VERY few cases. We started the project with an already populated database, so we just looked up records that fit our test requirements. This only takes you so far. We have also tried the mock approach that you describe.

    We just have not settled, as a team, on the best approach for our system.

    When we do settle, I will post about it.

  8. Just a few things.
    Listing.Find(1) will NOT go to the database, it is useful if you want to do something like:

    image.Listing = Listing.Find(1);

    Lazy loading will ensure that you save to the DB without having to load the object, at the cost of a potential FK violation error.

  9. Hi Adam – I just discovered your blog. It looks great. Added to my reader! We are just digging into MonoRail here and this was a great post to read. A couple thoughts:

    Do you read Jean-Paul Boodhoo’s blog (www.jpboodhoo.com)? He’s a certified (by me, at least) genius and has some good ideas about setting up builds and automated unit tests. I believe he takes the approach of creating and populating a test db for testing against. Having an intimate knowledge of the database you’re talking about though, I don’t know that that would work here – that would be a lot of test data/schema to manage.

    Have you considered straight NHibernate mappings for your model persistence (mapping domain objects to multiple tables)? Given the chance, I would definitely look into that for the app I work on. We currently use ActiveRecord, and while it saved us from a much worse homegrown Active Record Pattern based persistence layer, there is still much room for improvement. I didn’t realize that using the sessions negatively impacted performance that much, though. I’d be interested in hearing more on that. We basically have a repository pattern, so re-mapping shouldn’t be too horrible, and it’s something we may be discussing soon for performance reasons.

    Anyway, I think the stuff you guys do at Asynchrony is really cool. Looking forward to reading more of your blog.

  10. You could try NDbUnit to test the database access. I have a small program that reads the database using a dataset and then saves the dataset on two xml files: schema and data. NDbUnit uses these files to repopulate the database before each test.

  11. We have talked about NDbUnit, but we did not like using XML format that NDbUnit requires.

  12. Here’s the exception you get when you don’t have MARS enabled:

    at NHibernate.Loader.Loader.LoadCollection(ISessionImplementor session, Object id, IType type)
    at NHibernate.Loader.Collection.CollectionLoader.Initialize(Object id, ISessionImplementor session)
    at NHibernate.Persister.Collection.AbstractCollectionPersister.Initialize(Object key, ISessionImplementor session)
    at NHibernate.Impl.SessionImpl.InitializeCollection(IPersistentCollection collection, Boolean writing)
    at NHibernate.Collection.AbstractPersistentCollection.Initialize(Boolean writing)
    Exception: There is already an open DataReader associated with this Command which must be closed first.
    Stack Trace:
    at System.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
    at System.Data.SqlClient.SqlConnection.ValidateConnectionForExecute(String method, SqlCommand command)
    at System.Data.SqlClient.SqlCommand.ValidateCommand(String method, Boolean async)
    at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
    at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
    at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method)
    at System.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
    at System.Data.Common.DbCommand.System.Data.IDbCommand.ExecuteReader()
    at NHibernate.Impl.BatcherImpl.ExecuteReader(IDbCommand cmd)
    at NHibernate.Loader.Loader.GetResultSet(IDbCommand st, RowSelection selection, ISessionImplementor session)
    at NHibernate.Loader.Loader.DoQuery(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies)
    at NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies)
    at NHibernate.Loader.Loader.LoadCollection(ISessionImplementor session, Object id, IType type)

  13. That is it. Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: