Django's official documentation nicely covers its generic relationship functionality when you have to make an OneToMany (one-to-many or 1:N) relation, but when you need to implement a generic ManyToMany (many-to-many or N:N) relation, there is not much documentation about it.
I recently had to implement an N:N with a generic side, and discovered a library called django-gm2m that was very useful to me in this task, and I will describe here giving tips on how to make this implementation and how to avoid possible headaches.
This implementation was tested at Django 1.8 and 2.1 with python 2.7 and 3.7 respectively.
To demonstrate this modeling we will follow the concept of a digital library were the user profiles can get different formats of media like audio, video and text and the same media can be linked to more than one profile.
In this kind of scenario we have at one side N profiles and at the other side N medias at different formats and the models on Django are:
from django.db import models class Profile(models.Model): name = models.CharField(max_length=80) email = models.EmailField() class Audio(models.Model): title = models.CharField(max_length=60) records = models.PositiveSmallIntegerField() class Video(models.Model): title = models.CharField(max_length=60) resolution_x = models.PositiveSmallIntegerField() resolution_y = models.PositiveSmallIntegerField() class Text(models.Model): title = models.CharField(max_length=60) pages = models.PositiveSmallIntegerField()
django-gm2m run at your virtualenv or environment of choice:
$ pip install django-gm2m
After that make sure you have at your
django.contrib.contenttypes entry and add
INSTALLED_APPS = [ ... 'django.contrib.contenttypes', ... 'gm2m', ]
To create the relation add to the not generic side the field
from gm2m import GM2MField class Profile(models.Model): ... medias = GM2MField('Video', 'Audio', 'Text', related_name='profiles')
related_name will create the reverse relation at the other models so you can access the profiles trough any media instance. If you don't set it to anything it will follow the pattern
Another important point are the positional parameters of
GM2MField that are the models that you want Django mount the reverse relations automatically. In case of not setting the classes there the field will work but the reverse relation will not be mounted.
Now you are ready to add a media to a profile through
Eventually will be needed to add some field on the relation and to do that you will need to implement the relation model. To do that without blocking the
.add() function you need to set the
through parameter to define which model that will represent the relation:
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType class Profile(models.Model): ... medias = GM2MField( 'Video', 'Audio', 'Text', through='ProfileMediaLink', related_name='profiles' ) class ProfileMediaLink(models.Model): # required fields to mount the relation profile = models.ForeignKey(Profile) media = GenericForeignKey('media_ct', 'media_id') media_ct = models.ForeignKey(ContentType) media_id = models.PositiveIntegerField() # extras linked_at = models.DateField(auto_now_add=True)
See that for the generic side we use the same pattern from generic relations described at the official documentation of Django. A
GenericForeignKey composed by a
ContentType and a
PositiveIntegerField to keep the id for the model instance referenced.
To link a profile to a media you just need to do the following:
>>> ProfileMediaLink(profile=profile, media=video).save()
Generic relation is a powerful tool that allow us to achieve a high abstraction level. However its implementation can scale up at volume and complexity, so think a little before going to this kind of solution right upfront.