Monday, February 6, 2012

Mapping-by-Code - Map

The last not yet covered collection type supported in NHibernate 3.2 mapping-by-code is Map. I'll skip all the options that are common with set and bag and refer to its own post. For other collection types, see also the previous post.

Map is NHibernate's name for dictionary, or key-value collection. It is quite a powerful feature. NHibernate allows keys and values to be of different types - either single elements (like strings), components or entities. We can have an entity in the key and string in the value or string in the key and component in the value, etc. Moreover, as with every other collection, map can take part in one-to-many, many-to-many and even many-to-any relationships. There's quite a lot of options.

Mapping for all of these options must be scary, one may think. But not with mapping-by-code. I'm really impressed how easy, convenient and consistent with all the other features the Map mapping is. There's an recurring pattern in mapping-by-code that each nested element in XML has its corresponding options as a separate method parameter. And that's the case for Map, too. There are three basic overloads for Map method:

public void Map<TKey, TElement>(
Expression<Func<TEntity, IDictionary<TKey, TElement>>> property,
Action<IMapPropertiesMapper<TEntity, TKey, TElement>> collectionMapping);

public void Map<TKey, TElement>(
Expression<Func<TEntity, IDictionary<TKey, TElement>>> property,
Action<IMapPropertiesMapper<TEntity, TKey, TElement>> collectionMapping,
Action<ICollectionElementRelation<TElement>> mapping);

public void Map<TKey, TElement>(
Expression<Func<TEntity, IDictionary<TKey, TElement>>> property,
Action<IMapPropertiesMapper<TEntity, TKey, TElement>> collectionMapping,
Action<IMapKeyRelation<TKey>> keyMapping,
Action<ICollectionElementRelation<TElement>> mapping);

Don't be scared. These are just three variants of the same method ordered from the simplest one (with two parameters skipped - it'll have its default values) to the most complete one (with all four parameters). Let's focus on the last one.

The first parameter is - as always - the lambda expression pointing to a generic IDictionary property we are mapping. The second is an obligatory collection mapping - all the standard collection options (described in the post about Bag and Set) are available there. The third one, optional, is to set options for dictionary key mapping and the last one, also optional, is about its value mapping. Here it is, with all its options:

Map(x => x.Features, c =>
{
// standard collection options here
}, k =>
{
k.Element(e =>
{
e.Column("indexColumnName");
// or
e.Column(c =>
{
c.Name("indexColumnName");
// etc...
});

e.Formula("arbitrary SQL expression");
e.Length(10);
e.Type<CustomType>();
});
// or
k.Component(e =>
{
e.Property(x => x.KeyElement);
e.ManyToOne(x => x.OtherKeyElement);
// etc...
});
// or
k.ManyToMany(e =>
{
e.Column("indexColumnName");
// or
e.Column(c =>
{
c.Name("indexColumnName");
// etc...
});

e.ForeignKey("foreignKeyName");
e.Formula("arbitrary SQL expression");
});
}, r =>
{
// one of the following:
r.OneToMany();
r.Element();
r.Component(m => {});
r.ManyToMany();
r.ManyToAny<CommonIdType>(m => {});
});

Let's talk about key and value mappings (third and fourth parameter). In XML, there are plenty of different names for all its options, like map-key, composite-map-key, map-key-many-to-many etc. Mapping-by-code simplifies it drastically. We just need to choose what type of element our key is - either Element (for simple values), Component or ManyToMany (for cases when we have an entity as a key). We are already familiar with all the options inside. The same is for value mapping - we need to choose one of five possible relation types, depending on what do we have as a value in our dictionary. For the description of different relation types, see separate post - all the options are available here, too.

Moreover, the default ConventionModelMapper is smart enough to figure out most options just by looking at our model and in most cases we just don't need to specify the relation types and its options at all.

Fluent NHibernate's equivalent

In Fluent NHibernate, the name "Map" was already used for Property mapping. Instead we have several different methods in HasMany/HasManyToMany chains to be used - AsMap, AsEntityMap, AsSimpleAssociation, AsTernaryAssociation. Pretty hard to figure out what's what.

Mapping for the dictionary value and its options stays the same as in Bag/Set case. I'll focus on mapping different key types.

The first case is when we have simple value as a key - like IDictionary<string, string>:

HasMany(x => x.Dictionary)
.AsMap<string>("keyColumn")
.Element("valueColumn");

Majority of AsMap overloads want me to specify lambda expression pointing from the value to the key and internally use AsIndexedCollection method (designed for List, I believe). These methods seems to assume that key is a part of an object in value, what is strange a bit. In that case we don't really need to have a dictionary and we could just map that collection as a simple bag.

Moreover, FNH is not trying to determine the key type on its own - we need to specify it as a generic parameter in AsMap method explicitly, otherwise we'll end up with int. I also can't see the way to set other key options available in mapping-by-code, i.e. Length or Formula.

The second case I've tried was having a component as a key - and I've failed. I was looking through the Web and the source code itself and I can't see no composite-index equivalent.

Third type of objects allowed as a key is another entity - i.e. IDictionary<Entity, string>. We can map it using AsEntityMap:

HasMany(x => x.Dictionary)
.AsEntityMap()
.Element("valueColumn");

I was looking for any clues when should I use AsTernaryAssociation or AsSimpleAssociation directly, but I don't find any.

To sum things up, dictionary mapping in Fluent NHibernate is a horrible mess. I've found a comment in FNH source code: "I'm not proud of this. The fluent interface for maps really needs to be rethought. But I've let maps sit unsupported for way too long so a hack is better than nothing." Well, personally I'm not sure whether it'll be better in this case. Mapping by code is way much better this time.

20 comments:

  1. AsTernaryAssociation is used in dictionary|Entity, Entity| mappings.
    I've runned into the same issue: FNH dictionary mapping is a mess (try, for instance, to map dictionary|Entity, Component|). 13 hours of reading code and tests of FNH just for dictionary mapping and, moreover, this excellent post convinced me to use MbC instead of FNH.
    PS thank you for great job, further posts will be greatly appreciated.

    ReplyDelete
  2. Btw, how did you learned all this stuff, cause i couldn't find any documentation on MbC. Do you know any?

    ReplyDelete
  3. Thanks Alex. I don't know any documentation, too, that's why I'm writing this series :) I'm using code reading and "trial and error" methods.

    ReplyDelete
  4. Hi Adam,

    Thanks for the wonderful posts. They are helping me a lot.

    But I am stuck at one thing, can you please help me? How can I map these Entities:
    public class Foo
    {
    public virtual IDictionary Bars { get; set; }
    }

    public class Bar
    {
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    }

    Any help will be highly appreciated. Thanks. :)

    ReplyDelete
    Replies
    1. public virtual IDictionary<Bar, string> Bars { get; set; }

      Delete
    2. Interesting. API seems to support this with something like Map(x => x.Bars, m => {}, k => k.ManyToMany(), v => v.Element(...)), but it generates wrong mapping. Will investigate it further tomorrow.

      Delete
  5. Yes. It outputs "map-key-many-to-many" instead of "index-many-to-many" and element outputs a component instead. I think it is a bug.

    ReplyDelete
    Replies
    1. Filed a bug, https://nhibernate.jira.com/browse/NH-3102.

      Delete
  6. For map, how would you set index column?
    I've posted a question on stackoverflow: http://stackoverflow.com/questions/11159954/nhibernate-mapping-by-code-map-collection.

    ReplyDelete
  7. Hi,
    Is it possible with .AsMap/Element to map Dictionary with enum as string? Or can it only be mapped as int?

    ReplyDelete
  8. Hi. Is it possible to use SortedList as underlying collection, and if so how? Documentation says that comparer should be specified, but it did not help.

    ReplyDelete
    Replies
    1. I don't think so. Just map List, it is sorted by default in NHibernate and you can provide your own ordering, but not using comparer directly.

      Delete
  9. I need help:

    An association from the table XYZ refers to an unmapped class: System.String

    Map(x => x.Names, collectionMapping =>
    {
    collectionMapping.Table("NamesCity");
    collectionMapping.Key(k => k.Column("IdCity"));
    collectionMapping.Cascade(Cascade.All);
    },
    keyMapping => keyMapping.ManyToMany(e => e.Column("IdIdio")),
    mapping => mapping.Element(e => e.Column("Name")));

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. And how should your dictionary look like? What is the key there?

      Delete
    3. public class City
      {
      public virtual int IdCity { get; set; }
      public virtual IDictionary<Idioma, string> Names { get; set; }

      public City()
      {
      Names = new Dictionary<Idioma, string>();
      }
      }

      public class Idioma()
      {
      public virtual int IdIdio { get; set; }
      public virtual string Sigla { get; set; }
      }

      Delete
    4. Ah, it seems to be the same buggy scenario as discussed above: https://nhibernate.jira.com/browse/NH-3102.
      Still unresolved :/

      Delete
    5. IDictionary<int, string>
      It works

      Delete
  10. Adam: NH-3102 is now fixed in NH 4 and 3.4.

    ReplyDelete

Note: Only a member of this blog may post a comment.