57

I'm having trouble understanding the use of ManyToMany models fields with a through model. I can easily achieve the same without the ManyToMany field. Considering the following from Django's docs:

class Person(models.Model):
    name = models.CharField(max_length=128)


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')


class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

What I don't understand, is how is using the ManyToMany field better than simply dropping it and using the related manager. For instance, the two models will change to the following:

class Group(models.Model):
    name = models.CharField(max_length=128)


class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name='members')
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

What am I missing here?

Kritz
  • 7,099
  • 12
  • 43
  • 73

3 Answers3

67

You're right, if you define the membership table explicitly then you don't need to use a ManyToManyField.

The only real advantage to having it is if you'd find the related manager convenient. That is, this:

group.members.all()  # Persons in the group

looks nicer than this:

Person.objects.filter(membership_set__group=group)  # Persons in the group

In practice, I think the main reason for having both is that often people start with a plain ManyToManyField; realize they need some additional data and add the table explicitly; and then continue to use the existing manager because it's convenient.

Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
  • Excellent thanks. So it's down to preference which one I use? – Kritz Jun 06 '16 at 06:51
  • @Johan: That's right, it just depends on what kinds of queries you want to run and how you'd like to express them in the language of the ORM. Personally I tend to add the `ManyToManyField` just in case it's ever convenient to use that manager. – Kevin Christopher Henry Jun 06 '16 at 06:59
  • 7
    In addition, m2m relationship with through guarantee the uniquity of relation. In the question example, nothing prevent to have 2 memberships with same person/group references – Antwane Jan 25 '18 at 22:31
  • I have pressed -1 for this. I think this answer really needs improvements. Something in my answer bellow. But other thing: What is `membership`? From where is such name? If it should be an example of the m:n relation withouth `through=`, then we have only 2 models here: `group` and `person` :( – mirek Mar 16 '21 at 10:55
  • 2
    @mirek: Your criticism here, and your answer below, are based on a misunderstanding of the question. The OP isn't comparing using a explicit table or not. They're comparing using a m2m field or not in the presence of such a table. So to answer your question, `membership` exists in both versions. I've edited my answer slightly to make it clearer, but really you just need to re-read the question. – Kevin Christopher Henry Mar 16 '21 at 13:33
  • I actually ran into a use case. A user can belong to multiple clients, but there are two kinds of profile information: 1) Settings that apply to all clients (put in Profile table) 2) Settings specific to client (Put in Profile2Client, a through table) – davmor Dec 17 '21 at 20:29
  • You also get the convenience of adding and removing members via very simple methods such as `group.members.add(Stan)` or `group.members.remove(John)` which manage the relationship for you. – StanKosy Feb 01 '22 at 22:59
22

So I just wanted to add to anyone who is looking at this and may want another example to save them research. For one, I think it's important to note that in OP's questions, he should of removed the Group model not the People model and removed the matching field from the Membership model. That way, the model goes back to it's original meaning.

When looking at a many-to-many relationship, the through field can almost be contrived as the "why" to the many-to-many relationship. If we give the nomenclature a different name, it might change what the reader sees:

class Person(models.Model):
    name = models.CharField(max_length=128)


class Club(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='RegistrationReceipt')


class RegistrationReceipt(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    club = models.ForeignKey(Club, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)
    paid_dues = models.BooleanField(default = True)
    fee_payment_date = models.DateTimeField() 

Now, you can imagine yourself adding all sorts of logic whenever a member joins this club. When they joined? Why did they join? Did they pay? When is their payment date? etc. You can obviously tackle this relationship in different ways, but you can see more clearly the use of "through" in a Many-to-Many relationship.

Also, for those that know SQL. The through attribute/field is the way you customize the intermediary table, the one that Django creates itself, that one is what the through field is changing.

  • 6
    This should be the accepted answer, not only for pointing out the mistake in OP's example but showing the real benefit for the explicit through table – Cameron Sima Jun 10 '22 at 13:02
-3

I have some problem with the answer from Kevin Christopher Henry.

I don't think that the equivalent of the group.members.all() without a through="members" is Person.objects.....

Instead I think it is group.person_set.all() if the M2M field is on Person side. Or group.persons.all() if the M2M field is inside Group.

But I think without through=.. you have no control over the created table. It contains and will contain just 2 fields: both ID's of the related rows.

But with through=.. you can create the model yourself and add (now or later) the additional fields, which often can have a good reason. Example of such field: valid_from = DateField(), or so.

mirek
  • 1,140
  • 11
  • 10
  • Note that the last thing (about additional data ie. fields) is in the Kevin's answer too. – mirek Mar 16 '21 at 10:57