Components address a situation where multiple domain classes need to be mapped to a single database table. If you have the exact opposite situation, where a single domain entity needs to be mapped to multiple database tables, then you can use joins. This feature comes with a caveat though—the tables being joined must have a one-to-one relation between them. You would soon agree that this caveat makes sense.
To understand this better, we would assume that in our employee benefits domain model, we do not have an Address
entity, and all address fields are present on the Employee
entity itself. Moreover, we would also assume that in the database, address fields are stored in a different table named Address,
having a one-to-one relation with Employee
table. For the sake of simplicity, we would ignore other fields on Employee
entity. Following is how the Employee
entity with details relevant to this discussion looks:
public class Employee { public virtual int Id { get; set; } public virtual string AddressLine1 { get; set; } public virtual string AddressLine2 { get; set; } }
We only have an Id
field and a couple of more fields for address. We now want to map this entity such that fields related to address would come from a different table. If you are using mapping by code, then a function named Join
is available to specify that some of the properties of this entity need to be fetched by joining another table. The following code listing shows this mapping:
public class EmployeeMapping : ClassMapping<Employee> { public EmployeeMapping() { Id(e => e.Id); Join("tableName", joinMapper => { joinMapper.Table("Address"); joinMapper.Key(keyMapper => keyMapper.Column("Id")); joinMapper.Property(e => e.AddressLine1); joinMapper.Property(e => e.AddressLine2); }); } }
You are familiar with the most part of the preceding mapping code. The only new part is the call to Join
method. Let's drill a bit into that method. The first parameter to this method is a string which is a default table name. I have just passed tableName
here, as I intend to override this value later on, as we will see. The second parameter is a delegate that takes NHibernate.Mapping.ByCode.IJoinMapper<T>
as its parameter. In this case, it happens to be IJoinMapper<Employee>
. IJoinMapper
, which has several methods available on it that you can use to control how the Employee
and Address
tables are joined. We have used three methods from this interface. First one, Table
, is used to specify the name of the table to be joined. Second, Key
, is used to specify the name of the key column on the table. This information is used by NHibernate internally, when querying the table or inserting records into this table. The last method, Property
, is similar to the Property
method available during usual class mapping. This method is used to map properties to columns on the table.
We have the following test to verify the behavior of such a mapping:
[Test] public void AddressIsSavedCorrectly() { object id = 0; using (var tx = Session.BeginTransaction()) { id = Session.Save(new Employee { AddressLine1 = "address line 1", AddressLine2 = "address line 2" }); tx.Commit(); } Session.Clear(); using (var tx = Session.BeginTransaction()) { var employee = Session.Get<Employee>(id); Assert.That(employee.AddressLine1, Is.EqualTo("address line 1")); tx.Commit(); } }
We are inserting an instance of Employee
and then retrieving it by its Id
. The following SQL statements are generated by NHibernate for the preceding code:
INSERT INTO Employee (Id) VALUES (@p0); @p0 = 0 INSERT INTO Address (AddressLine1, AddressLine2, Id) VALUES (@p0, @p1, @p2); @p0 = 'address line 1', @p1 = 'address line 2', @p2 = 0 SELECT employee0_.Id AS Id0_0_, employee0_1_.AddressLine1 AS AddressL2_1_0_, employee0_1_.AddressLine2 AS AddressL3_1_0_ FROM Employee employee0_ INNER JOIN Address employee0_1_ ON employee0_.Id=employee0_1_.Id WHERE employee0_.Id=@p0;@p0 = 0
This is, as expected. You can notice that NHibernate used an inner join, as that is the default in this case. But if you want, you can change it to an outer join by specifying that the record in Address
table is optional.
There is not much to join mapping. You can map properties from an entity to as many as tables as you want. But remember that this is not recommended if you are working on a green field project. This feature has only been made available to work with legacy databases. In an ideal world, it is expected to have a lesser number of tables than the number of entities in the domain model.