Try fast search NHibernate

15 March 2010

ConfORM: “Mapping” Many-To-Many

In the last two post about ConfORM you saw how ConfORM follow you and your domain applying well know mapping-pattern without ask you to know it.

In this post you will see how ConfORM simplify the mapping process without need to know all NHibernate’s mapping elements.

Mapping for unidirectional association

The mapping for all unidirectional examples is:

public void MapDomain(ObjectRelationalMapper orm)
{
orm.TablePerClass(new[] { typeof(Person), typeof(Address) });
orm.ManyToMany<Person, Address>();
}

To set the cascade I can choose two ways:

  • by ObjectRelectionalMapper
  • by generic-customizer

Using the ObjectRelectionalMapper way I can define the cascade whichever is the relation from Person to Address:

orm.Cascade<Person, Address>(Cascade.All);
In these examples I will use the generic-customizer. The generic-customizer allows some customizations without know exactly which will be the real mapping. You don’t need to know if the class will be mapped as class,subclass,joined-subclass,union-subclass or a simple component. In the same way you can customize a collection without need to know if it will be a bag,set,array,list or a map(dictionary). The customization is:

public void CustomizeMapper(Mapper mapper)
{
mapper.Customize<Person>(cc => cc.Collection(person => person.Addresses, cm => cm.Cascade(Cascade.All)));
}

Unidirectional using Set<T>

The entities:

public class Person
{
private ISet<Address> addresses;
public Person()
{
addresses = new HashedSet<Address>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<Address> Addresses
{
get { return addresses; }
}
}

public class Address
{
public Guid Id { get; set; }
public string Street { get; set; }
public int CivicNumber { get; set; }
}

The mapping will be:

<class name="Person">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
set name="Addresses" access="field.camelcase" cascade="all">
<
key column="person_key" />
<
many-to-many class="Address" />
</
set>
</
class>
<
class name="Address">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Street" />
<
property name="CivicNumber" />
</
class>

As you can see, ConfORM have chose the right collection type, the right accessor and have applied the cascade.

Unidirectional using Bag<T>

The class modified:

public class Person
{
private ICollection<Address> addresses;
public Person()
{
addresses = new List<Address>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<Address> Addresses
{
get { return addresses; }
}
}

The new mapping:

<class name="Person">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
bag name="Addresses" access="nosetter.camelcase" cascade="all">
<
key column="person_key" />
<
many-to-many class="Address" />
</
bag>
</
class>

What I have done was only change the field and the constructor and again ConfORM have chose the right collection type, the right accessor and have applied the cascade.

Unidirectional using List<T>

The class modified:

public class Person
{
private IList<Address> addresses;
public Person()
{
addresses = new List<Address>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<Address> Addresses
{
get { return addresses; }
}
}

The new mapping:

<class name="Person">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
list name="Addresses" access="field.camelcase" cascade="all">
<
key column="person_key" />
<
list-index />
<
many-to-many class="Address" />
</
list>
</
class>

Again ConfORM have chose the right collection type, the right accessor and have applied the cascade.

If I need to customize the name of the column of the list-index instead usage the NHibernate’s convention (note: the naming convention is applied by NHibernate and not by ConfORM) I can’t do it by generic-customizer because the list-index is specific of lists collection (how do it through pattern-applier and/or through specific-customizer will be a matter of another post).

Unidirectional using Map<TK,TV>

The first change is using the Address as value of the dictionary:

public class Person
{
private IDictionary<string, Address> addresses;
public Person()
{
addresses = new Dictionary<string, Address>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public IDictionary<string, Address> Addresses
{
get { return addresses; }
}
}

The new mapping:

<class name="Person">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
map name="Addresses" access="nosetter.camelcase" cascade="all">
<
key column="person_key" />
<
map-key type="String" />
<
many-to-many class="Address" />
</
map>
</
class>

Now I’ll change the class in order to use the Address as the key of the dictionary:

public class Person
{
private IDictionary<Address, string> addresses;
public Person()
{
addresses = new Dictionary<Address, string>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public IDictionary<Address, string> Addresses
{
get { return addresses; }
}
}

The new mapping:

<class name="Person">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
map name="Addresses" access="nosetter.camelcase" cascade="all">
<
key column="person_key" />
<
map-key-many-to-many class="Address" />
<
element type="String" />
</
map>
</
class>

Resuming unidirectional many-to-many

Maintaining the same identical ConfORM-mapping (3 lines) I have changed radically the class implementation and ConfORM follows my needs.

Bidirectional many-to-many

In this case I will use a classic situation:

public class User
{
public Guid Id { get; set; }
public string Name { get; set; }
public ISet<Role> Roles { get; set; }
}

public class Role
{
public Guid Id { get; set; }
public string Name { get; set; }
public ISet<User> Users { get; set; }
}

As you can imagine even in this case the mapping is composed by two lines:

public void MapDomain(ObjectRelationalMapper orm)
{
orm.TablePerClass(new[] { typeof(User), typeof(Role) });
orm.ManyToMany<User, Role>();
}

As you can read in NHibernate’s reference manual, the bidirectional many-to-many follows a specific rule about the usage of “inverse”. If you don’t have time to read the manual, for this case, don’t worry because ConfORM know it and the mapping is:

  <class name="User">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
set name="Roles" table="RoleUser">
<
key column="user_key" />
<
many-to-many class="Role" />
</
set>
</
class>
<
class name="Role">
<
id name="Id" type="Guid">
<
generator class="guid.comb" />
</
id>
<
property name="Name" />
<
set name="Users" table="RoleUser" inverse="true">
<
key column="role_key" />
<
many-to-many class="User" />
</
set>
</
class>

In this mappings you can see two pattern-applier working:

  1. BidirectionalManyToManyTableApplier who is responsible to set the same table for both sides
  2. BidirectionalManyToManyInverseApplier who is responsible to set the inverse-end

Conclusion

In this post, more than show you how many magic-mapping you can do with 2 or 3 lines, I have introduced two new words of ConfORM as generic-customizer and pattern-applier.

Are you ConfORM ?

I don’t need to change myself to fit the framework, it fits to me!

8 comments:

  1. Excellent Fabio. I like to be ConfORM. What about mapping of class which work in differnet roles (interfaces) like Udi Dahan's Order and it's roles IOrderCalculator which should have eagerly fetched all order items?

    ReplyDelete
  2. I like this a lot, but there is a voice in the back of my head whispering "fluent nhibernate".

    Can you comment on way you are not using fluent with convention instead of ConfORM

    ReplyDelete
  3. @Martin Nyborg
    Because FNH does not fit my needs.

    Can you post your implementation and your results trying to recreate my examples but using FNH ?

    ReplyDelete
  4. << Because FNH does not fit my needs. >>
    This is what i expected to here and I respect that. It just seems that Oren Eini and now you have some bias against FNH. I can also see that in NH 3.0 there is a new fluent configuration API. Why don't the NH team build FNH into NH like the lambda extensions?
    I have 2 questions, is ConfORM going to bee a part of NH? and when can I expect a beta version?

    << Can you post your implementation and your results trying to recreate my examples but using FNH ?>>

    No I am to lazy :-) but I have to write more code than what you wrote. to do the same.

    ReplyDelete
  5. @Martin Nyborg
    For lazy people, lazy answers.
    "Because FNH does not fit my needs" is more than enough to explain why I need ConfORM.

    ReplyDelete
  6. Using ConfORM feels like magic, thanks you Fabio

    ReplyDelete
  7. Hi Fabio! Can you help me? I need to map ManyToMany, but, in my relationship table there are some extra colums. I'm using NH 3.0. I think this is like a IdBag in XML map. There are any way to do this?

    Please, save me :) haha

    Diego Ferreira.

    ReplyDelete