Modeling Relationships in a DDD Way

1. Types of relationships

There are 4 types of relationships between tables in a relational database:

  • One-to-many,
  • Many-to-one,
  • Many-to-many,
  • One-to-one.

1.1. One-to-many relationships

This is the most common type of relationship. To give an example, let’s say we have a Student and Enrollment tables in our database. The relationship between them would be one-to-many:

One-to-many relationship
  • For each student row, there can be multiple enrollment rows — This is what the second word in the X-to-Y notation means.
  • For each enrollment, there can be only one student — Which is what the first word in X-to-Y refers to.

1.2. Many-to-one relationships

The inverse of one-to-many is a many-to-one relationship. For each one-to-many relationship you automatically get a many-to-one relation that goes in the opposite direction. In the above example, it’s the relationship between enrollments and students.

Many-to-one relationship

1.3. Many-to-many

A many-to-many relationship would be between students and instructors, where each student can have many instructors and each instructor can also have many students:

Many-to-many relationship

1.4. One-to-one relationships

Finally, a one-to-one relationship is when each row in one table can only have one row in the related table, in both directions.

One-to-one relationship

2. Object-relational impedance mismatch

The above section shows how types of relationships look in a relational database: as associations between tables (represented by foreign key constraints).

public class Student
{
public string Name { get; }
public string Email { get; }
public ICollection<Enrollment> Enrollments { get; } // one-to-many
}

public class Enrollment
{
public string Grade { get; }
public string Course { get; }
public Student Student { get; } // many-to-one
}
  • To create an association between Student and Enrollment in the database, you only need to set up the enrollment's StudentID,
  • But in the domain model, you have to set up both, the student’s Enrollments and the enrollment's Student.
public class Student
{
public string Name { get; }
public string Email { get; }
public Course FavoriteCourse { get; } // many-to-one
}

public class Course
{
// no one-to-many
public string Name { get; }
}
Student student1 = GetStudent(1);
Student student2 = GetStudent(2);

var enrollment = new Enrollment
{
Student = student2 /* Setting student2 here... */
};

student1.Enrollments.Add(enrollment); /* ...but then using it for student1 */

3. One-to-one relationships

Let’s now talk about one-to-one relationships. In the domain model, such relationships are represented with a singular reference on both ends:

One-to-one relationship in the domain model
public class Student
{
public string Name { get; }
public string Email { get; }
public StudentDetails Details { get; } // one-to-one
}

public class StudentDetails
{
public string Address { get; }
public string Preferences { get; }
public string SportsParticipation { get; }
public Student Student { get; } // one-to-one
}
public class Student : Entity
{
public string Name { get; }
public string Email { get; }
public StudentDetails Details { get; }
}

public class StudentDetails : ValueObject
{
public string Address { get; }
public string Preferences { get; }
public string SportsParticipation { get; }
}
  • They are stored alongside the host entity, so no additional tables in the database are needed.
  • They are easier to work with because of their immutability.

4. Many-to-many relationships

Finally, let’s discuss many-to-many relationships. In the domain model, such relationships are represented by collections on both sides of the relation:

Many-to-many relationship
public class Student : Entity
{
public string Name { get; }
public string Email { get; }
public ICollection<Instructor> Instructors { get; } // many-to-many
}

public class Instructor : Entity
{
public string Name { get; }
public ICollection<Student> Students { get; } // many-to-many
}

4.1. Reducing parity of many-to-many relationships

So, what to do here? The first thing you need to consider is reducing parity of the many-to-many relationship by making it unidirectional instead of bidirectional.

public class Student : Entity
{
public string Name { get; }
public string Email { get; }

private readonly IList<Instructor> _instructors;
public IReadOnlyList<Instructor> Instructors => _instructors;

internal void AddInstructor(Instructor instructor)
{
_instructors.Add(instructor);
}
}

public class Instructor : Entity
{
public string Name { get; }

private readonly IList<Student> _students;
public IReadOnlyList<Student> Students => _students;

public void AddStudent(Student student)
{
_students.Add(student);
student.AddInstructor(this);
}
}

4.2. Working with intermediate tables in many-to-many relationships

Mapping of many-to-many relationships onto the domain model also raises issues. As I mentioned previously, there’s no notion of many-to-many relationship at the database level; that’s just two one-to-many relationships stacked upon each other with the help of an intermediate table.

  • If the intermediate table only contains references to the related tables, then don’t introduce a class for that table.
  • If the intermediate table contains other information, then do introduce a class for it.
public class Student : Entity
{
public string Name { get; }
public string Email { get; }

private readonly IList<StudentInstructor> _studentInstructors;
public IReadOnlyList<Instructor> Instructors => _studentInstructors
.Select(x => x.Instructor)
.OrderBy(x => x.DateAdded)
.ToList();

internal void AddInstructor(StudentInstructor instructor)
{
_studentInstructors.Add(instructor);
}
}

public class Instructor : Entity
{
public string Name { get; }

private readonly IList<StudentInstructor> _studentInstructors;
public IReadOnlyList<Student> Students => _studentInstructors
.Select(x => x.Student)
.OrderBy(x => x.DateAdded)
.ToList();

public void AddStudent(Student student)
{
var studentInstructor = new StudentInstructor(student, this, DateTime.Now);
_studentInstructors.Add(studentInstructor);
student.AddInstructor(studentInstructor);
}
}

public class StudentInstructor : Entity
{
public Student Student { get; }
public Instructor Instructor { get; }
public DateTime DateAdded { get; }

public StudentInstructor(Student student, Instructor instructor, DateTime dateAdded)
{
Student = student;
Instructor = instructor;
DateAdded = dateAdded;
}
}
Many-to-many intermediate table conversion

5. Summary

  • There are 4 types of relationships between tables in a relational database:
  • One-to-many,
  • Many-to-one,
  • Many-to-many,
  • One-to-one.
  • Try to reduce parity of the relationship by making it unidirectional instead of bidirectional.
  • Replace one-to-one relationships with value objects.
  • When working with intermediate tables in many-to-many relationships:
  • Don’t introduce a class for that table if it only contains references to related tables.
  • Do introduce a class for that table if it contains any other information. In this case, convert the many-to-many relationship into two one-to-many relationships.

Subscribe

Subscribe to read more articles like this: https://enterprisecraftsmanship.com/subscribe

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store