Wednesday, 1 April 2009

Conventions - After Rewrite

In December I have written a post about Conventions and AutoPersistenceModel in Fluent NHibernate. Since then lots of things have changed, especially with conventions, in this post I would like to show how to accomplish the same, old goals with new API.

To recap quickly, we have two following tables:

The issues which we are going to solve with conventions include:
AdventureWorks database doesn't follow the default convention. Differences:
  • table name is different then class name (Product vs Production.Product)
  • id property has different name then primary key column (Id vs ProductID)
  • properties representing links between tables have different names then foreign key column names (Product vs ProductID)

Here is how we can create conventions which are specific for AdventureWorks database:

IClassConvention

By default class name equals table name. If for some reason it's not true in your case then basically you have two options:
  • in each mapping class you can call WithTable(...) method and pass as a parameter your custom table name 
  • create your own convention by implementing IClassConvention
Of course second option is preferable in most cases, IClassConvention interface has to methods which need to be implemented:

   1:  public bool Accept(IClassMap target)
   2:  {
   3:      // apply this convention if table wasn't specified with WithTable(..) method
   4:      return string.IsNullOrEmpty(target.TableName);
   5:  }
   6:   
   7:  public void Apply(IClassMap classMap)
   8:  {
   9:      // table name can be like: 
  10:      // "Production." + class name  or  "Sales." + class name
  11:      // "Production" and "Sales" are the last parts of the namespace 
  12:   
  13:      var lastElementOfNamespace = classMap.EntityType.Namespace
  14:          .Substring(classMap.EntityType.Namespace.LastIndexOf('.') + 1);
  15:   
  16:      classMap.WithTable(String.Format("{0}.{1}", lastElementOfNamespace, classMap.EntityType.Name));
  17:  }

I think comments explain everything but I would like to stress the way Accept() method was implemented in. It's important to remeber that we don't always want to apply our conventions! If, for some reason, in mapping class WithTable(...) method was called to set specific table name then we shouldn't overwrite it. 

IIdConvention

Default convention for identity properties is also fairly straightforward - property name equals column name. If you need to change it then again there are two options:
  • in mapping class you can specify column name like this: Id(x => x.Id, "ProductID"); 
  • or implement IIdConvention and provide your own convention

   1:  public bool Accept(IIdentityPart target)
   2:  {
   3:      // make sure that column name wasn't set by calling Id(x => x.Id, "...")
   4:      return string.IsNullOrEmpty(target.GetColumnName());
   5:  }
   6:   
   7:  public void Apply(IIdentityPart target)
   8:  {
   9:      // primary key = class name + "ID"
  10:      target.ColumnName(target.EntityType.Name + "ID");
  11:  }
A few words of explanation, in database, primary keys are named like ProductID, ProductReviewID whereas in our class I want to stay with simple Id property for each class. Hence, to build column name it's required to use class name (for instance Product) and add "ID".

IReferenceConvention and IHasManyConvention

Last issue to deal with are foreign keys. ProductReview table has foreign key to Product table, as usually, property name (Product) is different then column name (ProductID) and therefore instead of specifing that (References(x => x.Product, "ProductID");) we can implement IReferenceConvention interface:


   1:  public bool Accept(IManyToOnePart target)
   2:  {
   3:      // make sure that column name wasn't set with References(x => x.Product, "...");
   4:      return string.IsNullOrEmpty(target.GetColumnName());
   5:  }
   6:   
   7:  public void Apply(IManyToOnePart target)
   8:  {
   9:      // foreign key column name = property name + "ID"
  10:      //
  11:      // it will be used in case like this:
  12:      // <many-to-one name="Product" column="ProductID" />
  13:   
  14:      target.ColumnName(target.Property.Name + "ID");
  15:  }

And to map easily other side of this association, without spcifying key column names we have to implement IHasManyConvention:

   1:  public bool Accept(IOneToManyPart target)
   2:  {
   3:      return target.KeyColumnNames.List().Count == 0;
   4:  }
   5:   
   6:  public void Apply(IOneToManyPart target)
   7:  {
   8:      // foreign key column name = class name + "ID" 
   9:      //
  10:      // it will be used to set key column in example like this:
  11:      //

  12:      // <bag name="ProductReview" inverse="true">
  13:      //   <key column="ProductID" />
  14:      //   <one-to-many class="AdventureWorksPlayground...ProductReview, AdventureWorksPlayground, ..." />
  15:      // </bag>
  16:   
  17:      target.KeyColumnNames.Add(target.EntityType.Name + "ID");
  18:      target.LazyLoad();
  19:      target.Inverse();
  20:  }

Please note that in last example we are not only setting key column name but also some other parameters like lazy load and inverse. 

Last words

As you can see with new API you need to write slightly more lines of code to achieve the same effect but I think it's a fair trade off for much greater control and flexibility. 

This post is just "touching" the subject, you can find much more about conventions and different interfaces on wiki. 

Related posts:

1 comment:

eric_ said...

I particularly like the way you are checking for an existing alteration in Accept().

I like the approach of setting exceptions to the rule in a class map override; others seem to prefer the approach of making new conventions to cover the exceptions, especially since FNH will overwrite any overrides with conventions unless you check first as you are. Checking for a pre-existing alteration allows one the freedom to choose which approach is best for a situation.

Question - I'm having trouble finding out how to check if a property's string length has already been set in Accept() though - do you know how to do that?

Great posting.