[Django]-Django models: how to overcome 'through' ManyToMany option limitation

5👍

I just worked through a similar problem which included an intermediate model with two foreign keys to the same target. This is what my system looks like:

class Node(models.Model):
    receivers = models.ManyToManyField('self', through='Connection',  related_name='senders',  symmetrical=False)

class Connection(models.Model):
    sender = models.ForeignKey(Node, related_name='outgoing')
    receiver = models.ForeignKey(Node, related_name='incoming')

I think this illustrates the main requirements for using two foreign keys to the same target in an intermediate model. That is, the model should have a ManyToManyField with the target 'self' (recursive ManyToMany) and the attribute through pointing to the intermediate model. I think it’s also necessary that each foreign key be assigned a unique related_name. The symmetrical=False argument applies to recursive relationships if you want them to be one-way, e.g. Node1 sends signals to Node2, but Node2 doesn’t necessarily send signals to Node1. It is necessary that the relationship be defined with symmetrical=False in order for a recursive ManyToMany to use a custom ‘through’ model. If you want to create a symmetrical recursive ManyToMany with a custom ‘through’ model, advice can be found here.

I found all these interrelationships fairly confusing, so it took me awhile to choose sensible model attributes and related_names that actually capture what the code is doing. To clarify how this works, if I have a node object N, calling N.receivers.all() or N.senders.all() return sets of other Nodes that receive data from N or send data to N, respectively. Calling N.outgoing.all() or N.incoming.all() access the Connection objects themselves, through the related_names. Note that there is still some ambiguity in that senders and receivers could be swapped in the ManyToManyField and the code would work equally well, but the direction becomes reversed. I arrived at the above by checking a test case for whether the ‘senders’ were actually sending to the ‘receivers’ or vice versa.

In your case, targeting both foreign keys to User adds a complication since it’s not obvious how to add a recursive ManyToManyField to User directly. I think the preferred way to customize the User model is to extend it through a proxy that’s connected to User through a OneToOneField. This is maybe unsatisfying in the same way that extending Membership with MembershipInfo is unsatisfying, but it does at least allow you to easily add further customization to the User model.

So for your system, I would try something like this (untested):

class Member(models.Model):
    user = models.OneToOneField(User, related_name='member')
    recruiters = models.ManyToManyField('self', through = 'Membership',  related_name = 'recruits',  symmetrical=False)
    other_custom_info = ... 

class UserManagedGroup(Group):
    leader = models.ForeignKey(Member, related_name='leaded_groups')
    members = models.ManyToManyField(Member, through='Membership', related_name='managed_groups')

class Membership(models.Model):
    member = models.ForeignKey(Member, related_name='memberships')
    made_member_by = models.ForeignKey(Member, related_name='recruitments')
    group = models.ForeignKey(UserManagedGroup, related_name='memberships')

    date_added = ...
    membership_justification = ...

The recursive field should be asymmetrical since Member1 recruiting Member2 should not also mean that Member2 recruited Member1. I changed a few of the attributes to more clearly convey the relationships. You can use the proxy Member wherever you would otherwise use User, since you can always access Member.user if you need to get to the user object. If this works as intended, you should be able to do the following with a given Member M:

M.recruiters.all() -> set of other members that have recruited M to groups
M.recruits.all() -> set of other members that M has recruited to groups
M.leaded_groups.all() -> set of groups M leads
M.managed_groups.all() -> set of groups of which M is a member
M.memberships.all() -> set of Membership objects in which M has been recruited
M.recruitments.all() -> set of Membership objects in which M has recruited someone

And for a group G,

G.memberships.all() -> set of Memberships associated with the group

I think this should work and provide a ‘cleaner’ solution than the separate MembershipInfo model, but it might require some tweaking, for example checking the direction of the recursive field to make sure that recruiters are recruiting recruits and not vice-versa.

Edit: I forgot to link the Member model to the User model. That would be done like this:

def create_member(member, instance, created, **kwargs):
    if created:
        member, created = Member.objects.get_or_create(user=instance)

post_save.connect(create_member, member=User)

Note that create_member is not a method of Member but is called after Member is defined. By doing this, a Member object should be automatically created whenever a User is created (you may need to set the member fields to null=True and/or blank=True if you want to add users without initializing the Member fields).

2👍

The simpliest way that I see is to remove the ManyToMany field from your UserManagedGroup and to merge Membership and MembershipInfo.

You will able to access your members as well with the entry_set fields.

Leave a comment