Search code examples
pythondjangodjango-modelsdjango-south

How to perform a datamigration when moving from one-to-one to one-to-many


I'm using south to manage migrations and I've hit a corner. Basically I have the following setup:

App1:

class A(models.Model):
  # bunch of attributes

App2:

class B(models.Models):
  instance_a = models.OneToOneField(A, null=True, blank=True, 
                                    editable=False)

Now, I want to go from this to this:

App1:

class A(models.Model):
  instance_b = models.ForeignKey(B, null=True, blank=True)

App2:

class B(models.Models):
  # other attributes

My main issue is that I can't loose data. So basically at the end of the migration(s) all objects A that mapped previously to objects B should keep that mapping. As an example, if object A with id 7 was mapped to object B with id 8, by the end of this procedure this mapping should be retained.

I tried several things from schema migrations mixed with temporary place holders and data migrations. However I end up always in the same place, which is by the time the data migration is performed I no longer have the previous relations in order to access the correct attributes. So for example, B.instance_a is no longer available.

I would like your opinion on two things:

  • First, is this viable at all using just south migrations.
  • Second, how shall I proceed.

Thanks


Solution

  • Finally after some time I got a procedure with django-south that might help others. The key was in south's depends_on feature (http://south.aeracode.org/wiki/Dependencies). I did it in 4 steps:

    First:

    • Create a placeholder for the values of the foreign key in model A.

    So model A becomes:

    class A(models.Model):
        instance_b_placeholder = models.ForeignKey(A, null=True, blank=True)
    

    Now just run manage.py schemamigration app1 --auto.

    Second

    • Create a datamigration so we can copy the values. The goal is to have the data duplicated in the db and later on rename the attributes and delete the old ones. Issue manage.py datamigration app1 update_fields. I chose to keep the datamigration in app1. If you don't do this just make sure it runs after the previous migration.

    Here's the datamigration coded:

    # Forwards:
    
    for b in orm['app2.B'].objects.filter(instance_b__isnull=False):
            b.instance_a.instance_b_placeholder = b
            b.instance_a.save()
    
    # Backwards:
    
    for r in orm['app1.A'].objects.filter(instance_b_placeholder__isnull=False):
            r.instance_b_placeholder.instance_a = r
            r.instance_b_placeholder.save()
    

    Third:

    • Delete the field instance_b from model B and be sure to make the migration run after the one created in the previous step.

    Model B becomes:

    class B(models.Model):
         # etc...
    

    Issue manage.py schemamigration app2 --auto and edit the migration adding the previous migration to depends_on:

    depends_on = (
        ("app1", "<migration_number>_update_fields"),
    )
    

    Forth step:

    • Rename the place holder. This is achieved by changing the name in the code and editing the migration. Editing is necessary because south tends to delete and add a new column, but we only want it to rename the column.

    • This migration should run in last place, so I made it dependent on the previous one.

    Here's the code:

    depends_on = (
        ("app2", "<previous_migration_here>"),
    )
    
    # Forwards:
    
        db.rename_column('app1_a', 'instance_b_placeholder_id', 'instance_b_id')
    
    # Backwards:
    
        db.rename_column('app1_a', 'instance_b_id', 'instance_b_placeholder_id')
    

    So that's it. I don't know if there are so many other ways to do it, but at least this helped me.