Sometimes there is a need to add more columns to a table, but the code model can't be updated to accommodate this. Can we still add the columns, persist, and query their values? Yes, NHibernate provides a way to do map table columns into a key-value IDictionary
, instead of individual properties of the class. This is called a dynamic-component.
DynamicComponents
to the MappingRecipes
project.Contact
to the folder:using System; using System.Collections; namespace MappingRecipes.DynamicComponents { public class Contact { public Contact() { Attributes=new Hashtable(); } public virtual Guid Id { get; protected set; } public virtual IDictionary Attributes { get; set; } } }
Contact.hbm.xml
to the folder:<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="MappingRecipes" namespace="MappingRecipes.DynamicComponents"> <class name="Contact"> <id name="Id"> <generator class="guid.comb"/> </id> <dynamic-component name="Attributes"> <property name="FirstName" type="string"/> <property name="LastName" type="string"/> <property name="BirthDate" type="DateTime"/> </dynamic-component> </class> </hibernate-mapping>
Recipe
to the folder:using System; using System.Linq; using NH4CookbookHelpers; using NHibernate; using NHibernate.Linq; namespace MappingRecipes.DynamicComponents { public class Recipe : HbmMappingRecipe { protected override void AddInitialData(ISession session) { session.Save(new Contact { Attributes = { ["FirstName"] = "Dave", ["LastName"] = "Gahan", ["BirthDate"] = new DateTime(1962, 5, 9) } }); session.Save(new Contact { Attributes = { ["FirstName"] = "Martin", ["LastName"] = "Gore", ["BirthDate"] = new DateTime(1961, 7, 23) } }); } public override void RunQueries(ISession session) { var contactsBornInMay = session.Query<Contact>() .Where(x => ((DateTime)x.Attributes["BirthDate"]).Month == 5) .ToList(); foreach (var contact in contactsBornInMay) { Console.WriteLine("{0} {1} {2:d}", contact.Attributes["FirstName"], contact.Attributes["LastName"], contact.Attributes["BirthDate"]); } } } }
DynamicComponents
recipe.Our Contact
class currently doesn't contain much more than an Id
property and an IDictionary
called Attributes
. Still, we are not only able to add data that is stored in specific column, we can also query the data, as if we had standard class properties.
The <dynamic-component>
mapping can be said to change the scope, from the properties of the class, to named elements in an IDictionary
. All the standard mappings are still available, so you are not limited to <property>
elements, but can add <many-to-one>
, collections and even nested dynamic components into the dictionary.
All the mappings within a dynamic component require that the mapped tables and columns exist, but the containing class never has to be modified in order to accommodate the new properties. Changes to the mapping and to the database can be performed without touching the code, for example by using non-embedded XML mapping files or by creating or modifying the mapping at runtime.
Storing the dynamic data could not be more straightforward. We simply add key value pairs to the IDictionary
and persist the object. Here is one possible syntax, using the IDictionary
initializer (which requires that the Attributes
property is assigned in the constructor):
session.Save(new Contact { Attributes = { ["FirstName"] = "Dave", ["LastName"] = "Gahan", ["BirthDate"] = new DateTime(1962, 5, 9) } });
If unmapped keys are added to the dictionary, their values will be discarded, just as would happen to an unmapped property.
Querying is a bit different. In the recipe, we use LINQ and query the Attributes
dictionary as is. We cast the value to a DateTime
, but only so that we can get access to the Month
sub property:
var contactsBornInMay = session.Query<Contact>() .Where(x => ((DateTime)x.Attributes["BirthDate"]).Month == 5) .ToList();
For HQL,CriteriaQueries
, and QueryOver
, we instead refer to the dynamic properties as if they were real properties. An HQL query would look like this:
var contactsBornInMay = session.CreateQuery(@" from Contact where month(Attributes.BirthDate)=:monthNumber") .SetInt32("monthNumber",5) .List<Contact>();
Multitenant applications are applications where many tenants (customers) share a common code base, and perhaps a common application process and database. If these tenants require custom properties on their entities, dynamic component mappings are a convenient way to provide just that. Adding a new property could involve these steps:
Points
is added, using a backend administration frontend.ClassMapping
or Fluent NHibernate.ISessionFactory
is created using the new mappings.Step 2 can be omitted if you preconfigure the table with generically named spare columns of the right types. For example, you can have columns named IntValue1
, IntValue2
and so on, and map these using <property name="Points" column="IntValue1" type="int"/>
. Just take care to add sensible default values (such as 0
) to these columns, or remember that they may be empty or null.
Perhaps we do not want to modify the core tables of the application. We can avoid this by enclosing the <dynamic-component>
mapping inside a <join>
mapping. The core table will be untouched, and all the custom attributes can live in a table, which can be unique to the configuration/tenant:
<join table="ContactAttributesForTenant23"> <key column="ContactId"/> <dynamic-component name="Attributes"> <property name="FirstName" type="string"/> <property name="LastName" type="string"/> <property name="BirthDate" type="DateTime"/> </dynamic-component> </join>
18.118.20.231