Skip to content

Instantly share code, notes, and snippets.

@cb109
Last active January 15, 2024 11:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cb109/4e753aa937ad1fa73f67ad9735875f5e to your computer and use it in GitHub Desktop.
Save cb109/4e753aa937ad1fa73f67ad9735875f5e to your computer and use it in GitHub Desktop.
Fix: ValueError: Cannot alter field <model>.<field> into <model>.<field> - they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M2M fields)
# Changing the through= part of a models.ManyToManyField() fails to migrate,
# as Django won't let us do this in a single step. We can manually workaround
# with several migration steps though, as shown below.
#
# Based on Django 3.2
# 1) Initial state of models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=128)
class Store(models.Model):
city = models.CharField(max_length=128)
products = models.ManyToManyField(Product, related_name="stores", blank=True)
# 2) Add a through model, e.g. as you want to track Products being sold out in a Store
class Product(models.Model):
name = models.CharField(max_length=128)
class Store(models.Model):
city = models.CharField(max_length=128)
products = models.ManyToManyField(
Product, related_name="stores", blank=True, through="StoreProduct"
)
class StoreProduct(models.Model):
store = models.ForeignKey("Store", on_delete=models.CASCADE)
product = models.ForeignKey("Product", on_delete=models.CASCADE)
sold_out = models.BooleanField(default=False)
# 3) Try running makemigrations then migrate, it should fail like this:
"""
ValueError: Cannot alter field core.Store.products into core.Store.products -
they are not compatible types (you cannot alter to or from M2M fields, or
add or remove through= on M2M fields)
"""
# 4) Revert your latest changes in models.py and remove the last migration file.
# Instead we add a copy of the original m2m field, copy the original mappings,
# then remove the old field, then rename the new field.
class Store(models.Model):
city = models.CharField(max_length=128)
products = models.ManyToManyField(Product, related_name="stores", blank=True)
products2 = models.ManyToManyField(
Product, related_name="stores2", blank=True, through="StoreProduct"
)
class StoreProduct(models.Model):
store = models.ForeignKey("Store", on_delete=models.CASCADE)
product = models.ForeignKey("Product", on_delete=models.CASCADE)
sold_out = models.BooleanField(default=False)
# 5) Run makemigrations and extend the migration manually like so:
from django.db import migrations, models
import django.db.models.deletion
def copy_store_products_to_new_field(apps, schema_editor):
Store = apps.get_model("core", "Store")
for store in Store.objects.filter(products__isnull=False):
store.products2.set(store.products.all())
class Migration(migrations.Migration):
dependencies = [
("core", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="StoreProduct",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("sold_out", models.BooleanField(default=False)),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.product"
),
),
(
"store",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.store"
),
),
],
),
# Copy the original field, but assign our new model as through=
migrations.AddField(
model_name="store",
name="products2",
field=models.ManyToManyField(
blank=True,
related_name="stores2",
through="core.StoreProduct",
to="core.product",
),
),
# Copy product associations from old field to new field
migrations.RunPython(
copy_store_products_to_new_field, migrations.RunPython.noop
),
# Remove old field
migrations.RemoveField(model_name="store", name="products"),
# Rename new field
migrations.RenameField(
model_name="store", old_name="products2", new_name="products"
),
# Use original related name
migrations.AlterField(
model_name="store",
name="products",
field=models.ManyToManyField(
blank=True,
related_name="stores",
through="core.StoreProduct",
to="core.Product",
),
),
]
# 6) Before running that migration, make sure to update Store in models.py to match it:
class Store(models.Model):
city = models.CharField(max_length=128)
products = models.ManyToManyField(
Product, related_name="stores", blank=True, through="StoreProduct"
)
# 7) Run migrate, you should end up with an m2m field named 'products' using existing
# associations, but also now your new through= model.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment