Custom primary key with Django ManyToManyField

Shaphil Mahmud
2 min readAug 5, 2021

Define your own primary key with ManyToManyField in Django and gain fine-grain control over the auto-generated intermediary table.

I was recently working on a Django project where I had to deal with a many-to-many relationship. So I decided to go with a ManyToManyField. While it did precisely what it was supposed to do, but not how I wanted it. It's probably better to demonstrate the problem with some code...

I had two models with a many-to-many relationship between them. My first model looks like this,

class BanglaWords(models.Model):
class Meta:
verbose_name_plural = 'Bangla Words'

bng_id = models.CharField(max_length=16, primary_key=True)
bangla_word = models.CharField(max_length=64)

def __str__(self):
return self.bangla_word

and the second one looks like,

class EnglishWords(models.Model):
class Meta:
verbose_name_plural = 'English Words'

eng_id = models.IntegerField(primary_key=True)
word = models.CharField(max_length=64)
bangla_word = models.ManyToManyField(BanglaWords)

def __str__(self):
return self.word

But the migration for this resulted in an intermediary table wordnet_englishwords_bangla_word which looked like this,

|----------------------------------|
| wordnet_englishwords_bangla_word |
|----------------------------------|
| id |
| englishwords_id |
| banglawords_id |
|__________________________________|

I didn’t want this however, I wanted bng_id to be the pk for this table. I solved the problem with the through argument of ManyToManyField. What the through argument allows you to do is to create the intermediary table (created automatically by ManyToManyField) yourself. You get finer control of how the intermediary table should look like, what fields it should have, and what its behavior should be. You can also define extra fields if you want. Here is the link to the official documentation on this, https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.ManyToManyField.through

I defined the intermediary model(table) myself and with the through argument, I pointed to the new intermediary model I created and instructed Django to create the table the way I wanted it.

First I created the intermediary model,

class BanglaEnglishRelations(models.Model):
class Meta:
verbose_name_plural = 'Bangla English Relations'

bng_id = models.OneToOneField('BanglaWords', primary_key=True, on_delete=models.CASCADE)
eng_id = models.ForeignKey('EnglishWords', on_delete=models.CASCADE)

which defines bng_id as the primary key as I desired.

Second, I told the ManyToManyField in EnglishWords to base the table on BanglaEnglishRelations like,

bangla_word = models.ManyToManyField(BanglaWords, through=BanglaEnglishRelations)

This resulted in the table wordnet_banglaenglishrelations which looked like,

|----------------------------------|
| wordnet_banglaenglishrelations |
|----------------------------------|
| bng_id_id |
| eng_id_id |
|__________________________________|

and served my purposes. This way you can promote whatever field to a pk for a table created with ManyToManyField in Django.

If you have a better idea or any suggestions for improvement, please don’t hesitate to leave a comment below.

Originally published at https://blog.shaphil.me on August 5, 2021.

--

--

Shaphil Mahmud

Fullstack Software Engineer. Lover of the backend. Friends with the Frontend. https://shaphil.me