3. n-n Child Mappings (Many to Many)

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:

  1. Insert all of the children and get their new ids

  2. Insert the parent and get its id

  3. 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.

3.1. Many-To-Many Explained

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);
		}
	}
}

Important

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.