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 { get; set; }
}
public class Customer : Entity
{
public virtual string CommercialName { get; set; }
public virtual string TaxId { get; set; }
public virtual IEnumerable<Order> Orders { get; set; }
}
public class Order : Entity
{
public virtual Customer Customer { get; set; }
public virtual DateTime EmissionDay { get; set; }
public virtual IEnumerable<OrderItem> Items { get; set; }
}
public class OrderItem : Entity
{
public virtual Order Order { get; set; }
public virtual string Product { get; set; }
public virtual decimal Price { get; set; }
}
{
public virtual int Id { get; set; }
}
public class Customer : Entity
{
public virtual string CommercialName { get; set; }
public virtual string TaxId { get; set; }
public virtual IEnumerable<Order> Orders { get; set; }
}
public class Order : Entity
{
public virtual Customer Customer { get; set; }
public virtual DateTime EmissionDay { get; set; }
public virtual IEnumerable<OrderItem> Items { get; set; }
}
public class OrderItem : Entity
{
public virtual Order Order { get; set; }
public virtual string Product { get; set; }
public virtual decimal Price { get; set; }
}
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);
}
}
{
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);
}
}
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();
mapper.AddMappings(Assembly.GetExecutingAssembly().GetExportedTypes());
HbmMapping domainMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
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();
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();
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);
}
};
{
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);
};
{
map.Cascade(Cascade.All.Include(Cascade.DeleteOrphans));
map.BatchSize(10);
};
public class MySimpleModelInspector: ExplicitlyDeclaredModel
{
public override bool IsOneToMany(MemberInfo member)
{
if(IsBag(member))
{
return true;
}
return base.IsOneToMany(member);
}
}
{
public override bool IsOneToMany(MemberInfo member)
{
if(IsBag(member))
{
return true;
}
return base.IsOneToMany(member);
}
}
var modelInspector = new MySimpleModelInspector();
var mapper = new ModelMapper(modelInspector);
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())
})));
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())
})));
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(), null, new HashedSet<string> { typeof(MsSql2005Dialect).FullName, typeof(MsSql2008Dialect).FullName });
}
{
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(), null, new 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
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
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-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>
Why you always use Bag()?
ReplyDeleteOrderItems shouldn't be Set() for performance reasons?
Is this the death of FluentNHibernate?
ReplyDeleteFabio thanks for this post I will learn alot from it ;)
ReplyDeleteDrop it away from the core!
ReplyDeleteFabio, I love you :)
ReplyDelete@Tonio,
ReplyDeleteshould I be worried ?
yes.. your soft sexy style is irresistibile for me :)
ReplyDelete@Tonio
ReplyDeleteFor your information:
finché c'ho il salame il prosciutto non lo incomincio.
:))) this is true italian lifestyle
ReplyDeleteI have a on old proejct that was started we;; before FNH or evem conform. It is further complicated by usinh nhibertnate castle facitlity.
ReplyDeleteIs there arny guidance on how to use the new and shinny with the old and xml.
In the CreateHighLowScript you have used Direct SQL Query, will it be possible to use a HQL query and use dialect to perform the translation.
ReplyDeleteI understand that the session interfaces cannot be used since session factory is still not up and running.
@Surya Pratap: There is no HQL for "Create Table". This is a plain old "auxiliary object". It used to be dialect specific (it means: you can write a different script for any dialect). I assume that this is still the case.
ReplyDeleteAnd if it wasn't the case: this is C#! You can write your own "if Dialect=SqlServer" switches all over the place. The sky is the limit.
@Fabio: It is really great. I already see some ways to make my project easier to maintain. E.g. It should theoretically be easy to write a single Enum custom type which gets an generic argument of the enum type and use it by convention. We also have our own kind or enums, which used to have a separate custom type each ...
ReplyDeleteIt is also easy to write your own "mapping framework", customized to your project. It will reduce the mapping code to the minimum.
PS: Can't you set hazzik to a black list? He's spamming ...
I am trying to convert from Fluent to ModelMapper but get exception below, even with the sample code above using 3.2 GA
ReplyDeleteNHibernate.MappingException: Could not determine type for: TestMap.Order, TestMap, for columns: NHibernate.Mapping.Column(id)
at NHibernate.Mapping.SimpleValue.get_Type()
at NHibernate.Mapping.SimpleValue.IsValid(IMapping mapping)
How can I set the ManyToOne and OneToMany mapping to either fetch\lazy\join? (as possible in raw mapping)
ReplyDeleteHi.. .
ReplyDeleteWhere can I found a updated wiki/manual or How To's for NHibernate 3.2
Regards
PS
This comment has been removed by the author.
ReplyDeleteHello, Fabio
ReplyDeleteI need your help) Can I define many-to-many relation with 'where' clause?
for example in FNH I can use ChildWhere() method. For example:
public class ProcedureMap : ClassMap
{
public ProcedureMap()
{
this.HasManyToMany(a => a.FormTemplates).ChildWhere("IsDeleted = 0").AsSet();
}
}