Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: convert ManyToManyField to ForeignKey

Let's supposed I created two models:

class Car(models.Model):
    name =  models.CharField(max_length=50)
    size =  models.IntegerField()


class Manufacturer(models.Model):
    name =  models.CharField(max_length=50)
    country =  models.CharField(max_length=50)
    car = models.ManyToManyField(Car)

I added entries to both models, then I realized that each Car was only related to a unique Manufacturer. So, I should convert my ManyToManyField to a ForeignKey:

class Car(models.Model):
    name =  models.CharField(max_length=50)
    size =  models.IntegerField()
    manufacturer = models.ForeignKey(Manufacturer)

class Manufacturer(models.Model):
    name =  models.CharField(max_length=50)
    country =  models.CharField(max_length=50)

How can I do that without losing my entries? I tried to look in South documentation but I did not found this way of conversion...

like image 861
gueux Avatar asked Dec 11 '22 19:12

gueux


2 Answers

This is nontrivial, I think you will need three migrations:

  1. Add the ForeignKey.
  2. Convert the ManyToMany to ForeignKey (using the forwards method).
  3. Remove the ManyToMany.

You could possibly merge 1 and 2 or 2 and 3 together, but I wouldn't recommend it.
Additionally, you should also implement a backwards method for 2.

An example for 2.'s forwards would be:

class Migration(SchemaMigration):
    def forwards(self, orm):
        for manufacturer in orm.Manufacturer.objects.all():
             for car in manufacturer.car.all():
                  car.manufacturer = manufacturer
                  car.save()

Please note that:

  • There is no backwards method here yet.
  • This needs to be tested extensively: a migration is something you should be extra careful about.
  • In case a car has two manufacturers, the last one will be kept.
  • This is very inefficient, we do a Query per car per manufacturer!

You will also need to update the code that uses those relationships in step 2. / 3.

like image 117
Thomas Orozco Avatar answered Dec 23 '22 10:12

Thomas Orozco


Based on the excellent answer provided by Thomas Orozco, I'd like to provide the solution for Django>=1.7 (basically the point 2 Convert the ManyToMany to ForeignKey is what varies in newer versions of Django). So here the code for the second migration:

class Migration(migrations.Migration):

    def migrate_m2m_to_fk(apps, schema_editor):
        Manufacturer = apps.get_model("app", "Manufacturer")
        for manufacturer in Manufacturer.objects.all():
             for car in manufacturer.car.all():
                  car.manufacturer = manufacturer
                  car.save()

    def migrate_fk_to_m2m(apps, schema_editor):
        Car = apps.get_model("app", "Car")
        for c in Car.objects.all():
            if c.manufacturer:
                c.manufacturer.car.add(c)
                c.manufacturer.save()

    operations = [
        migrations.RunPython(migrate_m2m_to_fk, migrate_fk_to_m2m)
    ]

Where "app" is the Django app where the models live. Both forward and reverse migration code is shown (as Thomas mentions, running this migrations may incur in loss of data, if more than one relationship was present beforehand, so please take care).

like image 45
vabada Avatar answered Dec 23 '22 10:12

vabada