Last active
June 13, 2024 21:43
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Update: Turns out there is a specific tool for this kind of migration that should | |
# do a better job as my multistep-migration below, check out SeparateDatabaseAndState: | |
# | |
# https://docs.djangoproject.com/en/5.0/howto/writing-migrations/#changing-a-manytomanyfield-to-use-a-through-model | |
# 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