N-N mappings mean that you have multiple objects on both sides. In the case of Northwind, a Territory can have multiple Employees assigned to it, and an Employee can have multiple Territories assigned to them. Because of the multi-sided relationship, it's known as n-n. A good indicator of a n-n relationship is when you have a "join" table for two tables. In the case of Northwind, it manifests itself in the following tables:
Table: Employees ========================= - EmployeeID - ... Table: EmployeeTerritories ========================= - EmployeeID - TerritoryID Table: Territories ========================= - TerritoryID - ...
The EmployeesTerritories table maps the many-to-many relationship. Any time you have a "middle" table that maps two other tables, it's a n-n relationship.
To support this, Elementary provides a [HasManyAndBelongsToMany] attribute. Let's create a simple mapping using the Northwind example. First, we'll create our Territory and Employee classes:
using System;
using System.Collections;
using System.Collections.Generic;
using Adapdev.Text;
using Elementary.Attributes;
namespace Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred
{
[ElementaryClass]
public class Employee{
[Persist]
public int EmployeeID = 0;
[Persist]
public string FirstName = "";
[Persist]
public string LastName = "";
[HasManyAndBelongsToMany(typeof(Territory))]
public List<Territory> Territories = new List<Territory>();
public override string ToString() {
return StringUtil.ToString(this);
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using Adapdev.Text;
using Elementary.Attributes;
namespace Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred
{
[ElementaryClass]
public class Territory
{
[Persist]
public int RegionID = 0;
[Persist]
public string TerritoryDescription = "";
[Persist]
public string TerritoryID = "";
[HasManyAndBelongsToMany(typeof(Employee))]
public List<Employee> Employees = new List<Employee>();
public override string ToString()
{
return StringUtil.ToString(this);
}
}
}
With that simple mapping in place, we can now get an Employee and all it's Territories, or vice versa. As with our previous examples, Elementary automatically infers the relationships and even the "middle" table. First, let's test the Employee object:
using System;
using System.Collections.Generic;
using System.Transactions;
using Elementary;
using Elementary.Northwind.Samples;
using NUnit.Framework;
namespace Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred
{
/// <summary>
/// Summary description for Test.
/// </summary>
[TestFixture]
public class Test
{
[TestFixtureSetUp]
public void SetUp()
{
Elementary.Clear();
Elementary.LoadAssembly("Elementary.Northwind.Samples.dll");
}
[Test]
public void GetEmployeeWithChildren()
{
IObjectMapper<Employee> mapper = Elementary.GetIObjectMapper<Employee();
// Retrieve supplier #1
Employee employee = mapper.GetObjectWithChildren(1);
Console.WriteLine(employee);
Console.WriteLine("Territories for " + employee.FirstName + ":");
foreach(Territory territory in employee.Territories)
{
Console.WriteLine(territory.TerritoryDescription);
}
}
}
}
Running this test results in the following output:
Properties for: Employee EmployeeID(System.Int32): 1 FirstName(System.String): Nancy LastName(System.String): Davolio Territories(System.Collections.Generic.List`1 [Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred.Territory]): System.Collections.Generic.List`1 [Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred.Territory] Territories for Nancy: Wilton Neward
As you can see, we retrieved all of the Territories for Nancy. Behind the scenes, this was done by essentially generating this query and mapping all of the objects:
SELECT * FROM Employees INNER JOIN EmployeeTerritories ON Employees.EmployeeID = EmployeeTerritories.EmployeeID INNER JOIN Territories ON EmployeeTerritories.TerritoryID = Territories.TerritoryID
Now, let's try the opposite. Let's retrieve all Employees assigned to Wilton - one of Nancy's territories. Since the primary key is the TerritoryId, we'll use that to get the Territory (#06897):
[Test]
public void GetTerritoryWithChildren()
{
IObjectMapper<Territory> mapper = Elementary.GetIObjectMapper<Territory>();
Territory territory = mapper.GetObjectWithChildren(06897);
Console.WriteLine(territory);
Console.WriteLine("Employees for " + territory.TerritoryDescription + ":");
foreach(Employee employee in territory.Employees)
{
Console.WriteLine(employee.FirstName);
}
}
You get the following output:
Properties for: Territory RegionID(System.Int32): 1 TerritoryDescription(System.String): Wilton TerritoryID(System.String): 06897 Employees(System.Collections.Generic.List`1 [Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred.Employee]): System.Collections.Generic.List`1 [Elementary.Northwind.Samples.Attributes.ChildMappingNToNGenericInferred.Employee] Employees for Wilton: Nancy
In the case of Wilton, only one Employee is assigned. But, we know we got it right since the relationship matches on both sides!
One of the more magical aspects of the many-to-many mapping is how it manages persistence. Traditionally, when writing custom data access layers, you would have to do the following:
Insert all of the children and get their new ids
Insert the parent and get its id
For each id, save the parent and child id to the mapping table (EmployeesTerritories in our example)
Fortunately, Elementary handles all of this for you! Let's look at an example:
[Test]
public void SaveWithChildrenAndUpdateChild()
{
IObjectMapper<Territory> mapper = Elementary.GetIObjectMapper<Territory>();
// Get an existing Employee
Employee employee = mapper.GetObjectWithChildren(1);
// Track the number of territories assigned
int count = employee.Territories.Count;
// Add a new territory
Territory territory = new Territory();
territory.RegionID = 1;
territory.TerritoryDescription = "Some Territory";
territory.TerritoryID = "12345";
employee.Territories.Add(territory);
// Modify an existing territory
Territory t = employee.Territories[0];
string tId = t.TerritoryID;
t.TerritoryDescription = "Bogus";
// Run in a transaction and rollback so that database changes aren't saved
using(TransactionScope scope = new TransactionScope())
{
mapper.SaveWithChildren(employee);
Employee employee2 = mapper.GetObjectWithChildren(1);
// Make sure the new territory was added
int newCount = employee2.Territories.Count;
Assert.AreNotSame(count, newCount, "Object wasn't saved.");
Assert.AreEqual(newCount, count+1, "Invalid save.");
// Make sure the modified territory was updated
Assert.AreEqual("Bogus", employee2.Territories[0].TerritoryDescription.Trim(), "Child wasn't updated.");
}
}
All of the regular operations that are available for 1-1 and 1-n mappings are available for n-n mappings.
The [HasManyAndBelongsToMany] attribute lets Elementary know that a n-n relationship exists. Elementary takes the current object's table name (Employees), the child object's table name (Territories), and tries to infer the mapping table (EmployeeTerritories). It will try the following patterns:
EmployeeTerritory
EmployeeTerritories
Employee_Territory
Employee_Territories
EmployeesTerritory
EmployeesTerritories
Employees_Territory
Employees_Territories
TerritoryEmployee
TerritoryEmployees
Territory_Employee
Territory_Employees
TerritoriesEmployee
TerritoriesEmployees
Territories_Employee
Territories_Employees
As you can see, it tries all combinations of SingleSingle, SinglePlural, Single_Single, Single_Plural, PluralSingle, PluralPlural, Plural_Single and Plural_Plural for each side of the relationship. If you want to explicitly define the table - which you must do if it doesn't match any of the patterns above - use the Through property:
using System;
using System.Collections;
using System.Collections.Generic;
using Adapdev.Text;
using Elementary.Attributes;
namespace Elementary.Northwind.Samples.Attributes.ChildMappingNToNGeneric
{
[ElementaryClass(Table="territories")]
public class Territory
{
[Persist]
public int RegionID = 0;
[Persist]
public string TerritoryDescription = "";
[Persist]
public string TerritoryID = "";
[HasManyAndBelongsToMany(typeof(Employee), "employeeterritories")]
public List<Employee> Employees = new List<Employee>();
public override string ToString()
{
return StringUtil.ToString(this);
}
}
}
When using "through" tables, Elementary assumes that the primary key for the parent and child will be the same in the "through" table. For example, Employees uses EmployeeID as the primary key. Elementary assumes that EmployeeID will be the column name in EmployeeTerritories...same goes for TerritoryID.