Bidirectional associations and ownership

In NHibernate parlance, bidirectional associations have a notion of ownership. Ownership defines how the foreign key that underpins the bidirectional association is updated when new items are added to an association in memory. You can declare any end of the association to be the owner. Your choice of ownership affects the SQL that is generated in order to synchronize the association correctly to database. Let's elaborate this with an example.

Following diagram depicts the bidirectional relationship between the Employee and Benefit class:

Bidirectional associations and ownership

The Employee to Benefit association is a bidirectional association which can also be seen as parent-child relation. Employee acts as parent and has multiple child benefits associated to it. From purely technical point of view, this is a one-to-many association. In one-to-many association, if ownership is not defined, then "one" side is considered as owner by default. So in our example, employee is owner of the association. What that tells NHibernate is that employee is responsible for setting this association up in the database correctly. So, for every benefit record that gets inserted into the database, employee would make sure that foreign key column Employee_Id on the Benefit table is updated with its own id. This is clear from the SQL that is generated by the following piece of code:

using (var tx = Session.BeginTransaction())
{
  var employee = new Employee();
  employee.AddBenefit(new SeasonTicketLoan
  {
    Amount = 1200,
    MonthlyInstalment = 100,
    StartDate = new DateTime(2014, 5, 12),
    EndDate = new DateTime(2015, 4, 12)
  });
  id = Session.Save(employee);

  tx.Commit();
}

The preceding code sends the following SQL to the database (stripped down to essential part for brevity):

Bidirectional associations and ownership

If you remember, we have seen this example earlier when we talked about setting both ends of bidirectional association. As we had noted back then, after inserting the Employee and Benefit record, an additional update statement is issued to update the Employee_Id column on the Benefit table. Think of it like this - because employee is responsible for making sure that foreign key column is updated correctly, employee would issue an additional update statement for every benefit item in its Benefits collection. This additional update is actually unnecessary and at times may impact performance. If we turn the tables around and make the Benefit entity the owner of the association, then this extra update statement goes away. So how do you make Benefit the owner of association? This is where the Inverse property of one-to-many association mapping comes in. Inverse is a Boolean property that you can declare on the one-to-many mapping, as shown next:

Set(e => e.Benefits,mapper =>
{
  mapper.Key(k => k.Column("Employee_Id"));
  mapper.Cascade(Cascade.All.Include(Cascade.DeleteOrphans));
  mapper.Inverse(true);
},
relation => relation.OneToMany(mapping => mapping.Class(typeof(Benefit))));

The preceding code listing is part of employee mapping. Note the code in bold where Inverse is set to true. This is telling NHibernate that other end of this association (Benefit in this case) is owner of the association and should be responsible for updating the foreign key in the database. After this change, if we run the preceding code again, we would notice that the extraneous update statement is no more generated.

So to summarize – in one-to-many association, if "one" side is declared as owner then you get an unnecessary update statement during insertion of new records. On the other hand, if "many" side is made the owner, the unnecessary update statement is not generated.

Note

In the preceding code, we passed employee instance to the session.Save() method. You could have passed the benefit instance as well here and got the same outcome. NHibernate does not care in which order you added entities to session or which entity is passed into the Save method. NHibernate uses cascade settings and navigates as deep into the association tree of the entity as possible to determine which entities to save or update. In case of one-to-many associations, it is easier to pass in the "one" side entity. If you choose to pass "many" side entity into the Save method then you would have to pass every entity in the collection. Imagine how your code would look when you have large number of entities on "many" side.

Ownership in many-to-many associations

As we know, many-to-many association is just a combination of two one-to-many associations. So ownership concepts that apply to one-to-many association also apply to many-to-many association. There are two differences though that you need to be aware of. To understand these differences, let's use many-to-many association between Employee and Community in our domain as example.

  • You have got collection mapping present on both the Employee and Community class mappings. So you can, in principle, set Inverse to true on both of these mappings. Setting Inverse to true on the Communities mapping of the Employee class tells NHibernate that the Employee class is not the owner of this association and hence not responsible for updating foreign key. Similar setting on the Members mapping of the Community class tells NHibernate that the Community class is not the owner of this association. Do you see where this is going? We end up telling NHibernate that none of the two sides own the association. The end result is that no records are inserted into the intermediate table holding the foreign keys.

    Though NHibernate would report that all entities in the association are saved successfully, in reality, the association would not be saved. So, always ensure that you set Inverse only on one end of the association.

  • When saving transient entities involving many-to-many association, association on the owner's end must be set. Failing to do that would result in no records being inserted into the intermediate table. In our case, if Employee is declared as the owner (mapping of the Members collection on the Community class has Inverse set to true) then the Communities collection on Employee must be initialized with the community instances that we want to persist. This can be confusing to beginners and experts alike, hence setting both ends of associations would save you from frustration when something goes wrong.

The preceding points really boil down to two simple guidelines. One - always set only one end as inverse. Two - always use convenience methods to add items to collection instead of directly accessing collections, so that both ends of the association are set correctly. Convenience methods for many-to-many collection should not be very different from those for one-to-many collection. I will leave it to readers as an exercise to come up with these methods.

Note

Inverse is probably not the best name to describe the behavior it offers. Also, the way this property works in not intuitive. Instead of being able to say that "this end is owner", you need to configure to say "the other end is owner". The negative nature of the property makes it even difficult for the beginners to understand how inverse works. There is not much we can do about it. Take your time and let the idea sink into your brain and then inverse would be second nature to you.

Inverse and cascade

In beginning, I used to get confused by cascade and inverse. I always thought that they have some relation and one impacts the other. But in reality, they are totally independent and have nothing to do with each other. Cascade determines whether the current action (save, update, delete and so on.) should be propagated down the association or not. On the other hand, inverse determines who is responsible for setting correct value in the foreign key column. Cascade settings can be applied to any type of associations whereas inverse can be specified only on the singular end of one-to-many association.

..................Content has been hidden....................

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