NHibernate Conventions

Posted by Ricardo Peres on ASP.net Weblogs See other posts from ASP.net Weblogs or by Ricardo Peres
Published on Tue, 18 Jun 2013 09:48:42 GMT Indexed on 2013/06/24 16:23 UTC
Read the original article Hit count: 529

Filed under:
|
|

Introduction

It seems that nowadays everyone loves conventions! Not the ones that you go to, but the ones that you use, that is! It just happens that NHibernate also supports conventions, and we’ll see exactly how.

Conventions in NHibernate are supported in two ways:

  • Naming of tables and columns when not explicitly indicated in the mappings;
  • Full domain mapping.

Naming of Tables and Columns

Since always NHibernate has supported the concept of a naming strategy. A naming strategy in NHibernate converts class and property names to table and column names and vice-versa, when a name is not explicitly supplied. In concrete, it must be a realization of the NHibernate.Cfg.INamingStrategy interface, of which NHibernate includes two implementations:

  • DefaultNamingStrategy: the default implementation, where each column and table are mapped to identically named properties and classes, for example, “MyEntity” will translate to “MyEntity”;
  • ImprovedNamingStrategy: underscores (_) are used to separate Pascal-cased fragments, for example, entity “MyEntity” will be mapped to a “my_entity” table.

The naming strategy can be defined at configuration level (the Configuration instance) by calling the SetNamingStrategy method:

   1: cfg.SetNamingStrategy(ImprovedNamingStrategy.Instance);

Both the DefaultNamingStrategy and the ImprovedNamingStrategy classes offer singleton instances in the form of Instance static fields. DefaultNamingStrategy is the one NHibernate uses, if you don’t specify one.

Domain Mapping

In mapping by code, we have the choice of relying on conventions to do the mapping automatically. This means a class will inspect our classes and decide how they will relate to the database objects. The class that handles conventions is NHibernate.Mapping.ByCode.ConventionModelMapper, a specialization of the base by code mapper, NHibernate.Mapping.ByCode.ModelMapper. The ModelMapper relies on an internal SimpleModelInspector to help it decide what and how to map, but the mapper lets you override its decisions.  You apply code conventions like this:

   1: //pick the types that you want to map
   2: IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetExportedTypes();
   3:  
   4: //conventions based mapper
   5: ConventionModelMapper mapper = new ConventionModelMapper();
   6:  
   7: HbmMapping mapping = mapper.CompileMappingFor(types);
   8:  
   9: //the one and only configuration instance
  10: Configuration cfg = ...;
  11: cfg.AddMapping(mapping);

This is a very simple example, it lacks, at least, the id generation strategy, which you can add by adding an event handler like this:

   1: mapper.BeforeMapClass += (IModelInspector modelInspector, Type type, IClassAttributesMapper classCustomizer) =>
   2: {
   3:     classCustomizer.Id(x =>
   4:     {
   5:         //set the hilo generator
   6:         x.Generator(Generators.HighLow);
   7:     });
   8: };

The mapper will fire events like this whenever it needs to get information about what to do. And basically this is all it takes to automatically map your domain! It will correctly configure many-to-one and one-to-many relations, choosing bags or sets depending on your collections, will get the table and column names from the naming strategy we saw earlier and will apply the usual defaults to all properties, such as laziness and fetch mode.

However, there is at least one thing missing: many-to-many relations. The conventional mapper doesn’t know how to find and configure them, which is a pity, but, alas, not difficult to overcome. To start, for my projects, I have this rule: each entity exposes a public property of type ISet<T> where T is, of course, the type of the other endpoint entity. Extensible as it is, NHibernate lets me implement this very easily:

   1: mapper.IsOneToMany((MemberInfo member, Boolean isLikely) =>
   2: {
   3:     Type sourceType = member.DeclaringType;
   4:     Type destinationType = member.GetMemberFromDeclaringType().GetPropertyOrFieldType();
   5:  
   6:     //check if the property is of a generic collection type
   7:     if ((destinationType.IsGenericCollection() == true) && (destinationType.GetGenericArguments().Length == 1))
   8:     {
   9:         Type destinationEntityType = destinationType.GetGenericArguments().Single();
  10:  
  11:         //check if the type of the generic collection property is an entity
  12:         if (mapper.ModelInspector.IsEntity(destinationEntityType) == true)
  13:         {
  14:             //check if there is an equivalent property on the target type that is also a generic collection and points to this entity
  15:             PropertyInfo collectionInDestinationType = destinationEntityType.GetProperties().Where(x => (x.PropertyType.IsGenericCollection() == true) && (x.PropertyType.GetGenericArguments().Length == 1) && (x.PropertyType.GetGenericArguments().Single() == sourceType)).SingleOrDefault();
  16:  
  17:             if (collectionInDestinationType != null)
  18:             {
  19:                 return (false);
  20:             }
  21:         }
  22:     }
  23:  
  24:     return (true);
  25: });
  26:  
  27: mapper.IsManyToMany((MemberInfo member, Boolean isLikely) =>
  28: {
  29:     //a relation is many to many if it isn't one to many
  30:     Boolean isOneToMany = mapper.ModelInspector.IsOneToMany(member);
  31:     return (!isOneToMany);
  32: });
  33:  
  34: mapper.BeforeMapManyToMany += (IModelInspector modelInspector, PropertyPath member, IManyToManyMapper collectionRelationManyToManyCustomizer) =>
  35: {
  36:     Type destinationEntityType = member.LocalMember.GetPropertyOrFieldType().GetGenericArguments().First();
  37:     //set the mapping table column names from each source entity name plus the _Id sufix
  38:     collectionRelationManyToManyCustomizer.Column(destinationEntityType.Name + "_Id");
  39: };
  40:  
  41: mapper.BeforeMapSet += (IModelInspector modelInspector, PropertyPath member, ISetPropertiesMapper propertyCustomizer) =>
  42: {
  43:     if (modelInspector.IsManyToMany(member.LocalMember) == true)
  44:     {
  45:         propertyCustomizer.Key(x => x.Column(member.LocalMember.DeclaringType.Name + "_Id"));
  46:  
  47:         Type sourceType = member.LocalMember.DeclaringType;
  48:         Type destinationType = member.LocalMember.GetPropertyOrFieldType().GetGenericArguments().First();
  49:         IEnumerable<String> names = new Type[] { sourceType, destinationType }.Select(x => x.Name).OrderBy(x => x);
  50:  
  51:         //set inverse on the relation of the alphabetically first entity name
  52:         propertyCustomizer.Inverse(sourceType.Name == names.First());
  53:         //set mapping table name from the entity names in alphabetical order
  54:         propertyCustomizer.Table(String.Join("_", names));
  55:     }
  56: };

We have to understand how the conventions mapper thinks:

  • For each collection of entities found, it will ask the mapper if it is a one-to-many; in our case, if the collection is a generic one that has an entity as its generic parameter, and the generic parameter type has a similar collection, then it is not a one-to-many;
  • Next, the mapper will ask if the collection that it now knows is not a one-to-many is a many-to-many;
  • Before a set is mapped, if it corresponds to a many-to-many, we set its mapping table. Now, this is tricky: because we have no way to maintain state, we sort the names of the two endpoint entities and we combine them with a “_”; for the first alphabetical entity, we set its relation to inverse – remember, on a many-to-many relation, only one endpoint must be marked as inverse; finally, we set the column name as the name of the entity with an “_Id” suffix;
  • Before the many-to-many relation is processed, we set the column name as the name of the other endpoint entity with the “_Id” suffix, as we did for the set.

And that’s it. With these rules, NHibernate will now happily find and configure many-to-many relations, as well as all the others. You can wrap this in a new conventions mapper class, so that it is more easily reusable:

   1: public class ManyToManyConventionModelMapper : ConventionModelMapper
   2: {
   3:     public ManyToManyConventionModelMapper()
   4:     {
   5:         base.IsOneToMany((MemberInfo member, Boolean isLikely) =>
   6:         {
   7:             return (this.IsOneToMany(member, isLikely));
   8:         });
   9:  
  10:         base.IsManyToMany((MemberInfo member, Boolean isLikely) =>
  11:         {
  12:             return (this.IsManyToMany(member, isLikely));
  13:         });
  14:  
  15:         base.BeforeMapManyToMany += this.BeforeMapManyToMany;
  16:         base.BeforeMapSet += this.BeforeMapSet;
  17:     }
  18:  
  19:     protected virtual Boolean IsManyToMany(MemberInfo member, Boolean isLikely)
  20:     {
  21:         //a relation is many to many if it isn't one to many
  22:         Boolean isOneToMany = this.ModelInspector.IsOneToMany(member);
  23:         return (!isOneToMany);
  24:     }
  25:  
  26:     protected virtual Boolean IsOneToMany(MemberInfo member, Boolean isLikely)
  27:     {
  28:         Type sourceType = member.DeclaringType;
  29:         Type destinationType = member.GetMemberFromDeclaringType().GetPropertyOrFieldType();
  30:  
  31:         //check if the property is of a generic collection type
  32:         if ((destinationType.IsGenericCollection() == true) && (destinationType.GetGenericArguments().Length == 1))
  33:         {
  34:             Type destinationEntityType = destinationType.GetGenericArguments().Single();
  35:  
  36:             //check if the type of the generic collection property is an entity
  37:             if (this.ModelInspector.IsEntity(destinationEntityType) == true)
  38:             {
  39:                 //check if there is an equivalent property on the target type that is also a generic collection and points to this entity
  40:                 PropertyInfo collectionInDestinationType = destinationEntityType.GetProperties().Where(x => (x.PropertyType.IsGenericCollection() == true) && (x.PropertyType.GetGenericArguments().Length == 1) && (x.PropertyType.GetGenericArguments().Single() == sourceType)).SingleOrDefault();
  41:  
  42:                 if (collectionInDestinationType != null)
  43:                 {
  44:                     return (false);
  45:                 }
  46:             }
  47:         }
  48:  
  49:         return (true);
  50:     }
  51:  
  52:     protected virtual new void BeforeMapManyToMany(IModelInspector modelInspector, PropertyPath member, IManyToManyMapper collectionRelationManyToManyCustomizer)
  53:     {
  54:         Type destinationEntityType = member.LocalMember.GetPropertyOrFieldType().GetGenericArguments().First();
  55:         //set the mapping table column names from each source entity name plus the _Id sufix
  56:         collectionRelationManyToManyCustomizer.Column(destinationEntityType.Name + "_Id");
  57:     }
  58:  
  59:     protected virtual new void BeforeMapSet(IModelInspector modelInspector, PropertyPath member, ISetPropertiesMapper propertyCustomizer)
  60:     {
  61:         if (modelInspector.IsManyToMany(member.LocalMember) == true)
  62:         {
  63:             propertyCustomizer.Key(x => x.Column(member.LocalMember.DeclaringType.Name + "_Id"));
  64:  
  65:             Type sourceType = member.LocalMember.DeclaringType;
  66:             Type destinationType = member.LocalMember.GetPropertyOrFieldType().GetGenericArguments().First();
  67:             IEnumerable<String> names = new Type[] { sourceType, destinationType }.Select(x => x.Name).OrderBy(x => x);
  68:  
  69:             //set inverse on the relation of the alphabetically first entity name
  70:             propertyCustomizer.Inverse(sourceType.Name == names.First());
  71:             //set mapping table name from the entity names in alphabetical order
  72:             propertyCustomizer.Table(String.Join("_", names));
  73:         }
  74:     }
  75: }

Conclusion

Of course, there is much more to mapping than this, I suggest you look at all the events and functions offered by the ModelMapper to see where you can hook for making it behave the way you want. If you need any help, just let me know!

© ASP.net Weblogs or respective owner

Related posts about nhibernate

Related posts about O/RM