NHibernate: playing with mapping by code

NHibernate: playing with mapping by code

NHibernate 3.2.0GA is going to be released and, around the NET, there are various questions about how use its “sexy-mapping”… “sexy-mapping” is not a serious definition? well… I’m bored by  zero-one definitions.

I think that the “problem” is that I have explicitly avoided to call something “best practice” writing  my examples (as I done with  ConfORM) and the new mapping-by-code is very flexible and you have various ways to achieve your target.
In this example I’ll try to resume the answers to various questions.

This time nobody sent me a domain so the case is not real… it is a domain just for  the exercise.



The domain

public  class  Entity
{
     public  virtual  int Id {  getset; }
}
public  class  Customer :  Entity
{
     public  virtual  string CommercialName {  getset; }
     public  virtual  string TaxId {  getset; }
     public  virtual  IEnumerable< Order> Orders {  getset; }
}
public  class  Order :  Entity
{
     public  virtual  Customer Customer {  getset; }
     public  virtual  DateTime EmissionDay {  getset; }
     public  virtual  IEnumerable< OrderItem> Items {  getset; }
}
public  class  OrderItem :  Entity
{
     public  virtual  Order Order {  getset; }
     public  virtual  string Product {  getset; }
     public  virtual  decimal Price {  getset; }
}

The incomplete class-by-class mapping

In our mapping-process we can start from various points depending on our knowledge of NHibernate, our knowledge of the domain, how much we known our conventions, how much we really known the relations between classes and so on. For this exercise I’ll start from a very incomplete class-by-class mapping as the follow:
public  class  EntityMapping<T> :  ClassMapping<T>  where T :  Entity
{
     public EntityMapping()
    {
        Id(x => x.Id);
    }
}
public  class  CustomerMapping :  EntityMapping< Customer>
{
     public CustomerMapping()
    {
        Property(x => x.CommercialName);
        NaturalId(map => map.Property(x => x.TaxId));
        Bag(x => x.Orders, map => map.Key(km => km.Column( "CustomerId")));
    }
}
public  class  OrderMapping :  EntityMapping< Order>
{
     public OrderMapping()
    {
        ManyToOne(x => x.Customer, map => map.Column( "CustomerId"));
        Property(x => x.EmissionDay, map => map.Type( NHibernateUtil.Date));
        Bag(x => x.Items, map => map.Key(km => km.Column( "OrderId")));
    }
}
public  class  OrderItemMapping :  EntityMapping< OrderItem>
{
     public OrderItemMapping()
    {
        ManyToOne(x => x.Order, map => map.Column( "OrderId"));
        Property(x => x.Product);
        Property(x => x.Price);
    }
}
If you have studied the mapping-by-code you know that you can define everything using class-by-class mapping but I want show you how little is the step between  explicit-mapping and  conventions/defaults-mapping.

The ModelMapper

In this exercise I’ll use the  ModelMapper. The ModelMapper delegates some responsibilities to others classes. You can see all classes involved analyzing all ModelMapper constructors overloads. If you have a look at the most simple constructor you will find that the ModelMapper uses, by default, the class  ExplicitlyDeclaredModel. The ExplicitlyDeclaredModel does not apply any special behavior other than what was explicitly declared or NHibernate’s convetions about table/columns names and types. Using class-by-class mapping the basic usage of the ModelMapper can look as:
var mapper =  new  ModelMapper();
mapper.AddMappings( Assembly.GetExecutingAssembly().GetExportedTypes());
HbmMapping domainMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
yes!, that is all… then you have to add the  domainMapping to the NHibernate’s configuration instance before BuildSessionFactory.

Applying tables-naming conventions over ModelMapper

The first convention required is : “All table names are lowercase”
Any kind of convention, customization have to be declared before get all mappings so we can do something like this:
var mapper =  new  ModelMapper();
mapper.AddMappings( Assembly.GetExecutingAssembly().GetExportedTypes());

mapper.BeforeMapClass += (mi, t, map) => map.Table(t.Name.ToLowerInvariant());
mapper.BeforeMapJoinedSubclass += (mi, t, map) => map.Table(t.Name.ToLowerInvariant());
mapper.BeforeMapUnionSubclass += (mi, t, map) => map.Table(t.Name.ToLowerInvariant());

HbmMapping domainMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
As you can see you can add all conventions even after add all mappings to the ModelMapper.

Applying property-types “conventions” over ModelMapper

“All properties defined as decimal have to be mapped as currency”
mapper.BeforeMapProperty += (mi, propertyPath, map) =>
{
     if ( typeof( decimal).Equals(propertyPath.LocalMember.GetPropertyOrFieldType()))
    {
        map.Type( NHibernateUtil.Currency);
    }
};

 

Applying collections “conventions” over ModelMapper

“All Bags are bidirectional-one-to-many and the batch size is 10”
mapper.BeforeMapBag += (mi, propPath, map) =>
{
    map.Cascade( Cascade.All.Include( Cascade.DeleteOrphans));
    map.BatchSize(10);
};
Is that enough to accomplish the target ? No, is not. We have defined only some attributes of the collections declared as  Bag but we haven’t defined which is the relation between the collection  ReflectedType and the collection-item-type. We can’t define the one-to-many relation using the ModelMapper because the responsibility to define/discover all relation is delegated to the  IModelInspector injected to the ModelMapper instance (note that I wrote  instance). Out-of-the-box you can find some implementations of  IModelInspector but if you want continue maintaining most of things under your control and you want learn the new mapping-by-code step-by-step you can simply use a class inherited from the  “”default””  ExplicitlyDeclaredModel. In this case the implementation can be simply:
public  class  MySimpleModelInspectorExplicitlyDeclaredModel
{
     public  override  bool IsOneToMany( MemberInfo member)
    {
         if(IsBag(member))
        {
             return  true;
        }
         return  base.IsOneToMany(member);
    }
}
and its usage is:
var modelInspector =  new  MySimpleModelInspector();
var mapper =  new  ModelMapper(modelInspector);

 

Applying POID strategy “conventions” over ModelMapper

At this point of the mapping-process we have a working an ready-to-work mapping for our above model but, where not defined, the default POID strategy is “assigned”. In this exercise I want use the  HighLow strategy (aka HiLo) but not the simple version; I want use the per-entity-High-Low strategy.
Knowing how we are giving the name of the table (we wrote it just few lines above) the mapping of the convention is:
mapper.BeforeMapClass += (mi, type, map) =>
    map.Id(idmap => idmap.Generator( Generators.HighLow,
        gmap => gmap.Params( new
        {
            table =  "NextHighVaues",
            column =  "NextHigh",
            max_lo = 100,
            where =  string.Format( "EntityName = '{ 0} '", type.Name.ToLowerInvariant())
        })));
Is it enough ? Yes! It is… mmm… perhaps it is enough but not for me. By default NHibernate will generates a table for HighLow but will not insert all records needed by  per-entity-High-Low. What we need is a smart IAuxiliaryDatabaseObject… something simple as:
private  static  IAuxiliaryDatabaseObject CreateHighLowScript( IModelInspector inspector,  IEnumerable< Type> entities)
{
     var script =  new  StringBuilder(3072);
    script.AppendLine( "DELETE FROM NextHighVaues;");
    script.AppendLine( "ALTER TABLE NextHighVaues ADD EntityName VARCHAR(128) NOT NULL;");
    script.AppendLine( "CREATE NONCLUSTERED INDEX IdxNextHighVauesEntity ON NextHighVaues (EntityName ASC);");
    script.AppendLine( "GO");
     foreach ( var entity  in entities.Where(x => inspector.IsRootEntity(x)))
    {
        script.AppendLine( string.Format( "INSERT INTO [NextHighVaues] (EntityName, NextHigh) VALUES ('{ 0} ',1);", entity.Name.ToLowerInvariant()));
    }
     return  new  SimpleAuxiliaryDatabaseObject(script.ToString(),  nullnew  HashedSet< string> {  typeof( MsSql2005Dialect).FullName,  typeof( MsSql2008Dialect).FullName });
}

 

Conclusions

Even if we started from a bored declarative (and incomplete) class-by-class-mapping we have changed its behavior without touch the initial mapping. The last step of the exercise is the integration with NHibernate3.2.0 and it is:
var configuration =  new  Configuration();
configuration.DataBaseIntegration(c =>
{
    c.Dialect< MsSql2008Dialect>();
    c.ConnectionString =  @"Data Source=localhost\SQLEXPRESS;Initial Catalog=IntroNH;Integrated Security=True;Pooling=False";
    c.KeywordsAutoImport =  Hbm2DDLKeyWords.AutoQuote;
    c.SchemaAction =  SchemaAutoAction.Create;
});
configuration.AddMapping(domainMapping);
configuration.AddAuxiliaryDatabaseObject(CreateHighLowScript(modelInspector,  Assembly.GetExecutingAssembly().GetExportedTypes()));

var factory = configuration.BuildSessionFactory();
// we are now ready to work with NHibernate
After build the session-factory in our db we will have:

For  Tommaso guys (non crede finché non tocca)
<? xml   version =" 1.0"   encoding =" utf-8" ?>
< hibernate-mapping   xmlns:xsi =" http://www.w3.org/2001/XMLSchema-instance"  
                    xmlns:xsd =" http://www.w3.org/2001/XMLSchema"  
                    namespace =" PlayWithMappingByCode"  
                    assembly =" PlayWithMappingByCode"  
                    xmlns =" urn:nhibernate-mapping-2.2" >
   < class   name =" Customer"   table =" customer" >
     < id   name =" Id"   type =" Int32" >
       < generator   class =" hilo" >
         < param   name =" table" >NextHighVaues </ param >
         < param   name =" column" >NextHigh </ param >
         < param   name =" max_lo" >100 </ param >
         < param   name =" where" >EntityName = 'customer' </ param >
       </ generator >
     </ id >
     < natural-id >
       < property   name =" TaxId"  />
     </ natural-id >
     < property   name =" CommercialName"  />
     < bag   name =" Orders"   cascade =" all,delete-orphan"   batch-size =" 10" >
       < key   column =" CustomerId"  />
       < one-to-many   class =" Order"  />
     </ bag >
   </ class >
   < class   name =" Order"   table =" order" >
     < id   name =" Id"   type =" Int32" >
       < generator   class =" hilo" >
         < param   name =" table" >NextHighVaues </ param >
         < param   name =" column" >NextHigh </ param >
         < param   name =" max_lo" >100 </ param >
         < param   name =" where" >EntityName = 'order' </ param >
       </ generator >
     </ id >
     < many-to-one   name =" Customer"   column =" CustomerId"  />
     < property   name =" EmissionDay"   type =" Date"  />
     < bag   name =" Items"   cascade =" all,delete-orphan"   batch-size =" 10" >
       < key   column =" OrderId"  />
       < one-to-many   class =" OrderItem"  />
     </ bag >
   </ class >
   < class   name =" OrderItem"   table =" orderitem" >
     < id   name =" Id"   type =" Int32" >
       < generator   class =" hilo" >
         < param   name =" table" >NextHighVaues </ param >
         < param   name =" column" >NextHigh </ param >
         < param   name =" max_lo" >100 </ param >
         < param   name =" where" >EntityName = 'orderitem' </ param >
       </ generator >
     </ id >
     < many-to-one   name =" Order"   column =" OrderId"  />
     < property   name =" Product"  />
     < property   name =" Price"   type =" Currency"  />
   </ class >
</ hibernate-mapping >

你可能感兴趣的:(Hibernate)