Dynamic components

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.

Getting ready

Complete the Getting ready instructions at the beginning of this chapter.

How to do it…

  1. Add a new folder named DynamicComponents to the MappingRecipes project.
  2. Add a new class named 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; }
      }
    }
  3. Add an embedded mapping named 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>
  4. Add a new class named 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"]);
          }
        }
      }
    }
  5. Run the application and start the DynamicComponents recipe.

How it works…

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>();

Note

At the time of writing, NHibernate only provides support for the non-generic IDictionary type, meaning that both the keys and the values are effectively exposed as object. An upcoming update (possibly 4.1) will also provide support for IDictionary<string,TValue>.

There's more…

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:

  1. An integer property named Points is added, using a backend administration frontend.
  2. The column is added to the database table, preferably using credentials with elevated permissions, which are not normally used by the application.
  3. The property is added to the mapping, usually using mapping by code ClassMapping or Fluent NHibernate.
  4. A new session factory ISessionFactory is created using the new mappings.
  5. The currently used session factory is replaced with the new, and can then be disposed.

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>
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.138.14