How to Migrate a ‘through’ to a many to many relation in Django

Mikail Kocak
2 min readMay 21, 2019

--

Here you are, wanting to migrate your newly added ‘through’ field to your M2M relation, but you run to the following error when doing so:

ValueError: Cannot alter field xxx into yyy they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M2M fields)

The invalid solution

Do not try to solve this error by doing a three-step migration:

  1. Creating the M2M model in the migrations
  2. Migrating the relations to the new model
  3. Deleting the previous relations

On the Internet, you will find people doing such things:

def create_through_relations(apps, schema_editor):
Collection = apps.get_model("product", "Collection")
CollectionProduct = apps.get_model(
"product", "CollectionProduct"
)
for collection in Collection.objects.all():
for product in collection.products.all():
CollectionProduct.objects.get_or_create(
product=product, collection=collection
)

It’s bad for two reasons:

  1. It destroys all the indexes;
  2. It’s heavy if there is a lot of data to migrate.

The Good Solution

Internally django is already using a ‘through’ model in M2M relation, but it doesn’t expose it. So we have to tell django that our new M2M ‘through’ model is the same one as django’s by changing the model’s database table name to the one automatically created by django during the previous migrations.

Here what your M2M’s through model looks like right now:

class CollectionProduct(SortableModel):
collection = models.ForeignKey(
"Collection", on_delete=models.CASCADE
)
product = models.ForeignKey(Product, on_delete=models.CASCADE)

And your model containing the M2M field:

class Collection(SeoModel, PublishableModel):
products = models.ManyToManyField(
Product,
blank=True,
related_name="collections",
through=CollectionProduct,
through_fields=["collection", "product"],
)

The solution

Take your app label (the package name, e.g. ‘product’) and your M2M field name, and combine them together with and underscore:

APPLABEL + _ + M2M TABLE NAME + _ + M2M FIELD NAME

For example in our case, it’s this:

product_collection_products

This is your M2M’s through database table name. Now you need to edit your M2M’s through model to this:

class CollectionProduct(SortableModel):
collection = models.ForeignKey(
"Collection", on_delete=models.CASCADE
)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
class Meta:
db_table = "product_collection_products"

Then your initial model creation migration, e.g.:

migrations.CreateModel(
name="CollectionProduct",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
)
],
),

Add the db_table name to the options of it:

options={"db_table": "product_collection_products"},

Which gives:

migrations.CreateModel(
name="CollectionProduct",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
)
],
options={"db_table": "product_collection_products"},
),

Here we go! Now simply run ./manage.py makemigrations <your app> and it will no longer attempt to alter your M2M relation anymore.

--

--