Many-to-many relationship between 3 tables in Django

Welcome to Programming Tutorial official website. Today - we are going to cover how to solve / find the solution of this error Many-to-many relationship between 3 tables in Django on this date .

I have the following models:

class Project(models.Model):
    project_name = models.CharField(max_length=50)
    project_users = models.ManyToManyField('Users.UserAccount', related_name='project_users', blank=True)
    ....

class UserAccount(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=30, unique=True)
    ....

class Discipline(models.Model):
    name = models.CharField(unique=True, max_length=27, choices=discipline_choices)

The DB table for project_users looks like this:

*--------*----------------*---------------------*
|   ID   |   project_id   |   user_account_id   |
*--------*----------------*---------------------*

I want to have an extra relationship in project_users field/table with the Discipline model. Is that possible in Django? I have been looking at intermediary-manytomany relationships in Django, but that’s not exactly what I want. In my application, there are a set amount of disciplines, and what I’m trying to achieve is to give each user multiple disciplines in a single project. So something like this:

*--------*----------------*---------------------*-------------------*
|   ID   |   project_id   |   user_account_id   |   discipline_id   |
*--------*----------------*---------------------*-------------------*

Is this possible?

Answer

You can make a mode that refers to the three models, that is in fact what a ManyToManyField does behind the curtains: creating a model in between.

class UserProjectDiscipline(models.Model):
    user_account = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE
    )
    discipline = models.ForeignKey(
        Discipline,
        on_delete=models.CASCADE
    )

You can now access the UserProjectDisciplines of a UserAccount, Project and/or Discipline with myuser.userprojectdiscipline_set.all(), myproject.userprojectdiscipline_set.all() and mydiscipline.userprojectdiscipline_set.all().

Furthermore you can define ManyToManyFields that span over this table, such that it is easy to obtain the related Projects of a Discipline, etc. with:

class Project(models.Model):
    project_name = models.CharField(max_length=50)
    users = models.ManyToManyField(
        'Users.UserAccount',
        through='UserProjectDiscipline',
        related_name='projects',
        blank=True
    )
    disciplines = models.ManyToManyField(
        'Discipline',
        through='UserProjectDiscipline',
        related_name='projects',
        blank=True
    )
    # …

class UserAccount(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=30, unique=True)
    disciplines = models.ManyToManyField(
        'Discipline',
        through='UserProjectDiscipline',
        related_name='users',
        blank=True
    ) 
    # …

These will then make JOINs on the table of the UserProjectDiscipline to retrieve efficiently users for a given project, etc. If you need objects for the UserProjectDiscipline, you can still use for example myproject.userprojectdiscipline_set.all().

If you thus want to give a user myuser three disciplines for a given project myproject, you add these with:

UserProjectDiscipline.objects.bulk_create([
    UserProjectDiscipline(user_account=myuser, project=my_project, discipline=mydiscipline1),
    UserProjectDiscipline(user_account=myuser, project=my_project, discipline=mydiscipline2),
    UserProjectDiscipline(user_account=myuser, project=my_project, discipline=mydiscipline3)
])