Support dynamic fields with NHibernate and .NET 4.0
A common theme in many application is the need to support custom / dynamic fields. In other words, the system admin may decide that the Customer needs to have a few additional fields that aren’t part of the mainline development.
In general, there are a few ways of handling that:
- DateField1, DateField2, StringField1, StringField2, etc, etc – And heaven helps if you need more than 2 string fields.
- Entity Attribute Value – store everything in a n EAV model, which basically means that you are going to have tables named: Tables, Rows, Attributes and Values.
- Dynamically updating the schema.
In general, I would recommend anyone that needs dynamic fields to work with a data storage solution that supports it (like RavenDB , for example). But sometimes you have to use a relational database, and NHibernate has some really sweet solution.
First, let us consider versioning. We are going to move all of the user’s custom fields to its own table. So we will have the Customers table and Customers_Extensions table. That way we are free to modify our own entity however we like it. Next, we want to allow nice syntax both for querying and for using it, even if there is custom code written against our code.
We can do it using the following code:
public class Customer { private readonly IDictionary attributes = new Hashtable(); public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual dynamic Attributes { get { return new HashtableDynamicObject(attributes);} } }
Where HashtableDynamicObject is implemented as:
public class HashtableDynamicObject : DynamicObject { private readonly IDictionary dictionary; public HashtableDynamicObject(IDictionary dictionary) { this.dictionary = dictionary; } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = dictionary[binder.Name]; return dictionary.Contains(binder.Name); } public override bool TrySetMember(SetMemberBinder binder, object value) { dictionary[binder.Name] = value; return true; } public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) { if (indexes == null) throw new ArgumentNullException("indexes"); if (indexes.Length != 1) throw new ArgumentException("Only support a single indexer parameter", "indexes"); result = dictionary[indexes[0]]; return dictionary.Contains(indexes[0]); } public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) { if (indexes == null) throw new ArgumentNullException("indexes"); if (indexes.Length != 1) throw new ArgumentException("Only support a single indexer parameter", "indexes"); dictionary[indexes[0]] = value; return true; } }
This is fairly basic so far, and not really interesting. We expose a hashtable as the entry point for a dynamic object that exposes all the dynamic fields. The really interesting part happens in the NHibernate mapping:
<class name="Customer" table="Customers"> <id name="Id"> <generator class="identity"/> </id> <property name="Name" /> <join table="Customers_Extensions" optional="false"> <key column="CustomerId"/> <dynamic-component name="Attributes" access="field.lowercase"> <property name="EyeColor" type="System.String"/> </dynamic-component> </join> </class>
As you can see, we used both a <join/> and a <dynamic-component/> to do the work. We used the <join/> to move the fields to a separate table, and then mapped those fields via a <dynamic-component/>, which is exposed an IDictionary.
Since we want to allow nicer API usage, we don’t expose the IDictionary directly, but rather expose a dynamic object that provides us with a nicer syntax.
The following code:
using (var session = sessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { session.Save( new Customer { Name = "Ayende", Attributes = { EyeColor = "Brown" } }); tx.Commit(); }
Will produce the following SQL:
And that is quite a nice solution all around
Comments
Very nice indeed!
Yeah, thats really cool and easy.
And btw it is good to read some interesting stuff again after a long time... :-)
I would say using some kind of IUserType (I do not remember if <dynamic-component allows that) to create the HashtableDynamicObject would be better.
Otherwise anyone who refactors the code to virtual dynamic Attributes { get; private set; } will break it, because it is not obvious that this field is used externally.
Nice post. Jeremy Miller has posted some good blog posts on the same topic.
codebetter.com/.../our-extension-properties-story/
codebetter.com/.../how-do-you-extend-and-custom...
So, I need to define each of dynamic field in the mapping? I think it is ugly.
I think the main advantage of this approach is the huge performance benefit over an EAV model. Unfortunately I don't see a way to get this working in a multi-tenant architecture, where different tenants need to have individual fields, while still using on the same DB.
Great stuff, in v3.0 NHibernate has really outgrown the "ORM" moniker and is on its way to becoming a platform.
@hazzik: I believe you could use ConfORM or Fluent NHibernate to do the mapping dynamically.
Daniel, I guess supporting different customers with different schemas on the same DB schema will always be an interesting proposition.
Hazzik,
Since the data storage in a RDBMS, you really have no choice.
Daniel,
You should use different schemas / databases for different tenants.
@Frank:
Whether different databases and schemas for different tenants are a good choice depends on the kind of software. Let's say you have a SaaS - based CRM-application and you want your customers to choose which fields make up a single business-contact. You need to find a way, to dynamically allow to customize the contact-entity.
In such a case it would not only be a waste of memory (each db-instance can take up to hundreds of mb) but also a unnecessary overhead while managing updates, backups, etc.
@Oren:
Would you say that RavenDB would be a good choice in such a situation?
looks nice.. but i don't like it that you need to write in the xml file properties of a dynamic field..
why... because it isn't dynamic any more.. if there is a new field you have to go in the code to add it..
that's why this is better to use it with mongodb because of the json solution. you don't need to use xml..or get in the code for adding a new field
add field detail page on asp.net and it add this field in the document at the monogdb.. you can even search these dynamic fields..
that's the real dynamic..
gr.
Janis,
I guess you missed the part where I recommended doing just that, then...
uuu..? you still using a xml mapper and need to edited the xml file for adding new fields...
i don't see that you are getting a collection from the database..
<property
has only one propertie.. it isn't a collection..
so if i need to add Eyesize i need to put
<property
in it :s....
oke.. you don't need to add it in the .net code.. but you have to edit in backend the xml file.. :S..
Janis,
I was actually referring to:
But in response to your other questions:
You can put a collection in a dynamic property.
You can edit the configuration programatically
Is there any way to combine this with NHibernate Lucene Full text search?
This is great for basic types.But can I add a many to one relation problematically.
How are the newcolumns created in customer_extensions.Is it Nhibernate doing update schema on the database.
Epraim,
Yes, it does.
Comment preview