Association between two classes is a very simple concept to understand. If you want to have association between ClassA
and ClassB,
then you can add a property of type ClassB
on ClassA
. This is the most basic form of association.
Associations come in four different forms as described next:
Collection
type. For example, IList<T>, [], ICollection<T>
,IEnumerable<T>
. and so onWe have example of all associations except many-to-many in our domain. Let me refer to part of the Employee
class and the Benefit
class as follows:
public class Employee : EntityBase { public virtual ICollection<Benefit> Benefits { get; set; } public virtual Address ResidentialAddress { get; set; } } public class Benefit : EntityBase { public virtual Employee Employee { get; set; } } public class Address : EntityBase { public virtual Employee Employee { get; set; } }
The Benefits
property on the Employee
class is an example of one-to-many association. On one side of this association we have got one instance of the Employee
class and on the other side we have got a list of instances of the Benefit
class.
On the Benefit
class, we have an association from benefit to employee. This is the other end of one-to-many association we just discussed. This clearly is many-to-one association. We can have multiple instances of the Benefit
class belonging to the same Employee
instance. The ResidentialAddress
property on the Employee
class, on the other hand, is an example of one-to-one association. Every employee has only one address. Similarly, association from Address
back to Employee
is also one-to-one for the same reason.
We do not have any example of many-to-many in our domain so I am going to extend the scope of our problem and add the following new business requirement to it:
Organization operates different communities for employees. Employees are encouraged to be members of different communities. These communities not only help in building a social circle but also give employees an opportunity to meet like-minded people and build on their skills by sharing experience. Employees can become members of more than one community at a time.
To satisfy the preceding requirement, we would need to add a new class to our domain to represent an employee community. There can be multiple members of a community and an employee can be a member of multiple communities. Following is the relevant part of the code:
namespace Domain { public class Community : EntityBase { public virtual string Name { get; set; } public virtual string Description { get; set; } public virtual ICollection<Employee> Members { get; set; } } } namespace Domain { public class Employee : EntityBase { public virtual ICollection<Community> Communities { get; set; } } }
So, we have a collection of the Community
class on Employee
representing the communities that the employee is member of. At the same time, we have a collection of the Employee
class on Community
holding all employees who are member of that community.
Before we actually look into mapping different types of associations, it would be worthwhile to spend some time understanding how a relational database supports associations. We know that every entity in the domain model gets mapped to a database table. So in order to support association between two classes, the database must be able to relate two database tables somehow. Relational databases achieve this via a foreign key relationship between two tables. If I could use association between the Employee
and Benefit
class as an example, then following database diagram explains how these two entities are related to each other in the database:
As you can see, column Employee_Id
on the Benefit
table is designated as a foreign key to the Employee
table. The value held in this column is primary key/identifier of a record in the Employee
table. You can repeat the same primary key in as many Benefit
records as you want. This implies that for one record in the Employee
table, you can store many records in the Benefit
table. This is basis of a one-to-many relationship.
Fundamentally, databases only support one-to-many relationships and one-to-one relationship using shared primary keys. All other relationships we discussed previously are merely a different arrangement of one-to-many relationship to achieve different results. For example, by putting a logical constraint that "you would never insert more than one record in the Benefit
table for a record in the Employee
table" you achieve a one-to-one relationship. Similarly, you can combine two one-to-many relationships through an intermediate table to achieve a many-to-many relationship.
Next, we will look at how the associations between the classes are mapped to database relations. As with previous mapping exercises, we will use unit tests to confirm that our mappings achieve what we want it to achieve.
The Benefits
collection on the Employee
class is the one-to-many association that we are going to map here. We will use the following unit test to drive the implementation of mapping. In this test, we add an instance of the Leave
, SeasonTicketLoan
, and SkillsEnhancementAllowance
classes to the Benefits
collection on the Employee
class and store the employee instance in database. We then retrieve the instance and confirm that all the benefit instances are present:
[Test] public void MapsBenefits() { object id = 0; using (var transaction = session.BeginTransaction()) { id = session.Save(new Employee { EmployeeNumber = "123456789", Benefits = new HashSet<Benefit> { new SkillsEnhancementAllowance { Entitlement = 1000, RemainingEntitlement = 250 }, new SeasonTicketLoan { Amount = 1416, MonthlyInstalment = 118, StartDate = new DateTime(2014, 4, 25), EndDate = new DateTime(2015, 3, 25) }, new Leave { AvailableEntitlement = 30, RemainingEntitlement = 15, Type = LeaveType.Paid } } }); transaction.Commit(); } session.Clear(); using (var transaction = session.BeginTransaction()) { var employee = session.Get<Employee>(id); Assert.That(employee.Benefits.Count, Is.EqualTo(3)); transaction.Commit(); } }
This test is not very different from the previous test we saw. After retrieving the saved employee instance from the database, we are confirming that the Benefits
collection on it has three items, which is what we had added. Let's add mappings to make this test pass. Mappings for this association would be added to the existing mapping file for the Employee
class, which is Employee.hbm.xml
. Following is how one-to-many association mapping is declared:
<set name="Benefits" cascade="all-delete-orphan"> <key column="Employee_Id" /> <one-to-many class="Benefit"/> </set>
One-to-many mapping is also called collection mapping in NHibernate parlance. This is because property being mapped usually represents a collection of items. The only mandatory attribute that needs to be declared on set
element is name
of the property which is being mapped. This happens to be Benefits
in this case.
There are two nested elements inside the set
node. First one is key,
which is used to declare the name of the foreign key column on the other end of the association. If you recall from the preceding diagram, foreign key from Benefit
to the Employee
class is named Employee_Id
.
Second node inside the set is one-to-many
. This is where we tell more about the many sides of this association. The only mandatory detail that is needed is name
of the class on the other end.
You may have noticed that I have also added a cascade
attribute on the mapping above. This is more of a personal preference and you can easily do away with this, but then remember that the default value that NHibernate assumes would be none
.
I believe "pictures speak a thousand words". In the following image, I have tried to explain how mapping relates to database tables and domain classes:
We have used set element to declare collection mapping. Set tells NHibernate that a property being mapped is a collection. Additionally, set also puts restrictions that the collection is unordered, unindexed, and does not allow duplicates. You can declare the collection to be ordered, indexed, and so on, using the following XML elements:
Bag
List
Map
Array
While they all map the collection properties to database, there are subtle differences in their runtime behavior and .NET types that they support. Following table gives key comparison of these mappings:
Set |
Bag |
List |
Map |
Array | |
---|---|---|---|---|---|
Duplicates |
Duplicates not allowed in the collection |
Duplicates allowed in the collection |
Duplicates allowed in the collection |
Duplicates not allowed in collection |
Duplicates allowed in collection |
Ordering |
Not ordered |
Not ordered |
Ordered |
Ordered |
Ordered |
Access |
Collection items cannot be accessed by index |
Collection items can be accessed by index |
Collection items can be accessed by index |
Collection items can be accessed by index |
Collection items can be accessed by index |
Supported .NET types |
|
|
|
|
|
Knowledge of the above key differences, coupled with knowledge of some other features offered by each of the collection mappings is useful in determining which collection mapping does the job best. I do not intend to cover those details here, as purpose is to only introduce you to collection mappings. As we go into more details in the coming chapters, we will revisit collection mappings and look into some of these features.
All association mappings have quite a detailed level configuration available. I have preferred to show you only the minimum required to make the associations work. Some of the optional configurations will be covered at appropriate times throughout the book. Rest are used in rare and specific situations and are left for readers to explore on their own.
The other end of one-to-many association is many-to-one association. When both one-to-many and many-to-one associations are present between two entities, it is called bidirectional association. This is because you can navigate from one end to the other end in both directions. Benefit
to the Employee
association in our domain model is an example of many-to-one association. This is also a bidirectional association if you notice. Reason to highlight bidirectional nature of association is that NHibernate handles them in a different way. We are going to cover this aspect in Chapter 5, Let's Store Some Data into the Database, but it is worth highlighting now what bidirectional mapping is.
To test this mapping, we will extend the unit test from the earlier collection mapping example and add the following lines after the line where we asserted that benefit count is 3:
var seasonTicketLoan = employee.Benefits.OfType<SeasonTicketLoan>(). FirstOrDefault(); Assert.That(seasonTicketLoan, Is.Not.Null); if (seasonTicketLoan != null) { Assert.That(seasonTicketLoan.Employee.EmployeeNumber, Is.EqualTo("123456789")); } var skillsEnhancementAllowance = employee.Benefits .OfType<SkillsEnhancementAllowance>().FirstOrDefault(); Assert.That(skillsEnhancementAllowance, Is.Not.Null); if (skillsEnhancementAllowance != null) { Assert.That(skillsEnhancementAllowance.Employee.EmployeeNumber, Is.EqualTo("123456789")); } var leave = employee.Benefits.OfType<Leave>().FirstOrDefault(); Assert.That(leave, Is.Not.Null); if (leave != null) { Assert.That(leave.Employee.EmployeeNumber, Is.EqualTo("123456789")); }
At first look, this code may look complex but it is not actually. We have loaded employee instance from the database. On this instance, we confirmed that list of benefit has 3 benefit instances present. We query this list using LINQ and retrieve benefit instance of each type. On each of the benefit instance, we confirm that the
Employee
instance is present and it is the same employee instance that we had saved to database.
The mapping for this association is relatively easy as compared to collection mapping. Following is how you would declare this mapping inside of the Benefit
class's mapping file, benefit.hbm.xml
:
<many-to-one name="Employee"class="Employee"column="Employee_Id"/>
The node many-to-one
signifies that this is a many end of a one-to-many association. There are only two mandatory attributes that this mapping takes. First one, name
identifies the property on the mapped class that is at the singular end of the association. Second, class
identifies the type of the singular end.
If the preceding text was any difficult to understand, then see if the following picture does any justice:
In one-to-one association, both ends of association hold a single item. There are two variations of one-to-one association in relational databases. First is a variation of one-to-many association in that it uses a foreign key from one table to another. Difference from one-to-many association is that this foreign key has unique constraint applied to it so that only one record can be present on many side. Second variation uses a shared primary key approach in which associated tables share the primary key value. We are only going to look at unique foreign key approach here. Shared primary key approach is left as an exercise for the readers.
We will use the Employee
to Address
association to guide us. To elaborate the database relationship that we just discussed, let me show you database diagram for the Employee
and Address
table and how they are associated using the foreign key Employee_Id
from the Address
table to the Employee
table.Refer to the following image:
We will use the following unit test in order to verify mappings for the preceding association:
[Test] public void MapsResidentialAddress() { object id = 0; using (var transaction = Session.BeginTransaction()) { var residentialAddress = new Address { AddressLine1 = "Address line 1", AddressLine2 = "Address line 2", Postcode = "postcode", City = "city", Country = "country" }; var employee = new Employee { EmployeeNumber = "123456789", ResidentialAddress = residentialAddress }; residentialAddress.Employee = employee; id = Session.Save(employee); transaction.Commit(); } Session.Clear(); using (var transaction = Session.BeginTransaction()) { var employee = Session.Get<Employee>(id); Assert.That(employee.ResidentialAddress.AddressLine1, Is.EqualTo("Address line 1")); Assert.That(employee.ResidentialAddress.AddressLine2, Is.EqualTo("Address line 2")); Assert.That(employee.ResidentialAddress.Postcode, Is.EqualTo("postcode")); Assert.That(employee.ResidentialAddress.City, Is.EqualTo("city")); Assert.That(employee.ResidentialAddress.Country, Is.EqualTo("country")); Assert.That(employee.ResidentialAddress.Employee.EmployeeNumber,Is .EqualTo("123456789")); transaction.Commit(); } }
You might have noticed that I have explicitly set the ResidentialAddress
property on the Employee
class and the Employee
property on the Address
class. This is a bidirectional association and NHibernate usually makes sure that both ends are persisted correctly even if one end is set in code. But this is not always true in case of one-to-one association. We would cover details like these in Chapter 5, Let's Store Some Data into the Database, when we talk about persisting entities. I just wanted to highlight that the unit test for this scenario is written slightly differently.
So we have got a one-to-one association from Employee
to Address
and another one from Address
back to Employee
. Association from Employee
to Address
is mapped as follows:
<one-to-one name = "ResidentialAddress" class = "Address" property-ref = "Employee" cascade = "all" />
By now, you may have guessed what node one-to-one
and attributes name
, class
, and cascade
are for. The only additional attribute property-ref
is used to declare the name of the property on the other end of the association which refers back to this entity. In our example, that would be property named Employee
on Address
class.
Association from Address
to Employee
is actually many-to-one association constrained to single item. It is mapped using many-to-one XML node we have seen earlier with an additional attribute specifying the unique constraint.
<many-to-one name="Employee" class="Employee" column="Employee_Id" unique="true" />
Following diagram should help you relate how this mapping associates domain model and database tables:
In database, many-to-many associations are just an arrangement of two one-to-many associations connected via an intermediate table. Employee communities' example from our domain could translate to a table schema which looks as follows:
As you can see, we have got a connecting table named Employee_Community
. There are two one-to-many relationships at play here, one going from Employee
to Employee_Community
and the other going from Community
to Employee_Community
. The end result is that if we navigate from Employee
through to Community
, we may end up with multiple communities and same if we navigate the other way round. Thus we get many-to-many relationship.
Following unit test verifies the behavior we want when employee to community association is mapped:
[Test] public void MapsCommunities() { object id = 0; using (var transaction = session.BeginTransaction()) { id = session.Save(new Employee { EmployeeNumber = "123456789", Communities = new HashSet<Community> { new Community { Name = "Community 1" }, new Community { Name = "Community 2" } } }); transaction.Commit(); } session.Clear(); using (var transaction = session.BeginTransaction()) { var employee = session.Get<Employee>(id); Assert.That(employee.Communities.Count, Is.EqualTo(2)); Assert.That(employee.Communities.First().Members.First().EmployeeNumber, Is.EqualTo("123456789")); transaction.Commit(); } }
Here we are storing an instance of the Employee
class with two instances of the Community
class. We then retrieve the saved instance of Employee
and verify that it has two instances of Community
present on it. We also verify that community has employee instance in its Members
collection. There are probably more things we can verify here but for the purpose of testing associations, I think this is enough.
Mapping for this association needs to be added on both Employee
and Community
end. This is because the association is bidirectional. Following is the mapping added to employee.hbm.xml
:
<set name="Communities" table="Employee_Community" cascade="all-delete-orphan"> <key> <column name="Employee_Id" /> </key> <many-to-many class="Community"> <column name="Community_Id" /> </many-to-many> </set>
Next is the other part of the mapping added to community.hbm.xml
:
<set name="Members" table="Employee_Community" cascade="all-delete-orphan" inverse="true"> <key> <column name="Community_Id"/> </key> <many-to-many class="Employee"> <column name="Employee_Id" /> </many-to-many> </set>
The preceding mappings are quite similar to the collection mappings we saw earlier. There is no surprise there as we are really dealing with two collection mappings. Let's just note the key differences in this mapping from the collection mapping.
First difference is the additional table
attribute on the set node. This attribute tells NHibernate what the name of the connecting table is.
Second difference is the node many-to-many
inside the set node. In collection mapping, we had one-to-many
here. Two mandatory pieces of information on the many-to-many
node are class
and column
. class
tells the name of the class on the other end of association. In employee mappings this is Community
and in community this is Employee
. column
is used to declare the name of the foreign key column on the connecting table that relates to the other end of the association. For employee side, this is column Community_Id
on the connecting table. For community side, this is column Employee_Id
on the connecting table.
Again, the subsequent images might help. The following first image shows Employee
to Community
association mapping:
Next, the second image shows Community
to Employee
association mapping:
Similar to collection mapping, set is used as an example here and you can use map, bag, list, array, and so on.
We have the inverse
attribute added to the mappings on Community
side. This attribute controls who owns the association. I intend to cover this in detail in Chapter 5, Let's Store Some Data into the Database, so do not be bothered by inverse at this stage.
We have reached the end of association mapping for this chapter. Whatever we learned here should form a good foundation of knowledge. Associations are a vast and important area. We will keep revisiting association mappings throughout the book and learn more about them in the process. For the time being, let's move on to next topic.