10

I have an existing Django project that has several models using concrete inheritance of a base class. After closer consideration, and after reading about what people like Jacob Kaplan-Moss have to say about it, using this concrete inheritance is unnecessary in my case. I would like to migrate to using an abstract base class instead.

The thing that makes this complicated is that my site is live and I have user entered data. Thus, I'll need to keep all my data intact throughout this transition.

I'll give an example to be more concrete:

Before:

app1/models.py:

class Model1(base_app.models.BaseModel):
    field1 = models.CharField(max_length=1000)
    field2 = models.CharField(max_length=1000)

app2/models.py:

class Model2(base_app.models.BaseModel):
    field1 = models.CharField(max_length=1000)
    field2 = models.CharField(max_length=1000)

base_app/models.py:

class BaseModel(models.Model):
    user = models.ForeignKey(User)
    another_field = models.CharField(max_length=1000)

After:

app1/models.py:

class Model1(base_app.models.BaseModel):
    field1 = models.CharField(max_length=1000)
    field2 = models.CharField(max_length=1000)

app2/models.py:

class Model2(base_app.models.BaseModel):
    field1 = models.CharField(max_length=1000)
    field2 = models.CharField(max_length=1000)

base_app/models.py:

class BaseModel(models.Model):
    user = models.ForeignKey(User)
    another_field = models.CharField(max_length=1000)

    class Meta:
        abstract = True

Right now, my plan is to first add the abstract = True to the BaseModel. Then,for each model that uses BaseModel, one at a time:

  • Use south to migrate the database and create this migration using the --auto flag
  • Use a south data migration. For instance, I would loop through each object in Model1 to fetch the object in BaseModel that has the same pk and copy the values for each field of the BaseModel object to the Model1 object.

So first, will this work? And second, is there a better way to do this?

Update:

My final solution is described in detail here:

http://www.markliu.me/2011/aug/23/migrating-a-django-postgres-db-from-concrete-inhe/

Spike
  • 5,040
  • 5
  • 32
  • 47

2 Answers2

7
  1. Add NewBaseModel, we use different name so it doesn't conflict with current non-abstract one (South would actually delete BaseModel otherwise).

    class NewBaseModel(models.Model):
        user = models.ForeignKey(User)
        another_field = models.CharField(max_length=1000)
    
        class Meta:
            abstract = True
    
  2. Set Model1 and Model2 to inherit from NewBaseModel

  3. Run schemamigration --auto, 2 new fields will be added to Model1 and Model2
  4. Run datamigration --empty and fill new fields from values in BaseModel
  5. Load production db and double check everything migrated correctly
  6. Remove BaseModel and rename NewBaseModel to BaseModel
  7. Run schemamigration --auto (this should work ;) )
  8. Deploy!

NOTE: Use orm variable when migrating to use current state of your model schema.

Seb
  • 17,141
  • 7
  • 38
  • 27
  • Thanks for pointing out how South will delete BaseModel. You made me really glad I asked this question! I'll be doing this within the next week or two and will report back with how it goes. – Spike Aug 21 '11 at 03:41
  • I'm marking this as accepted since you steered me on the right track. There are a number of pitfalls in doing this, but I managed to get things working after a long night last night. There ended up being quite a few more steps. I wrote more about it here: http://markliu.me/2011/aug/23/migrating-a-django-postgres-db-from-concrete-inher/ – Spike Aug 23 '11 at 21:56
  • 2
    Just realized my link was broken. Here's the new one: http://www.markliu.me/2011/aug/23/migrating-a-django-postgres-db-from-concrete-inhe/ – Spike Feb 26 '13 at 16:31
1

Sebastjan Trepča's answer is probably good but, another way to do it will be to create your migration manually:

  1. Add the abstract = True to your base model.

  2. Run schemamigration --auto, the generated migration will probably not be good but you will use it as a base.

  3. Edit the migration file. In the forward you should add, in this order:

    a. db.delete_foreign_key(table_name, column) for each of your children models. This will remove the ForeignKey between the parent and the children table.

    b. db.delete_table(BaseModel) to delete the table of the base model (this command should be probably there already, generated by --auto).

    c. It's possible that you will have to rename all the primary key column of your children models to 'id'. I'm not sure about this. If you need to do this: db.rename_column(table_name, column_name, 'id') for each of your children models.

  4. Remove all auto-generated code in forward that doesn't make sense.

  5. Run the migration: migrate

What this method is doing is removing the table of the base class and the foreign keys between the base class table and its children because they are not use with the Abstract Base Class.

I didn't test this method so it's possible that you'll hit some problems. This approach is more complicated then the other one but the advantages are that you don't need to migrate the data and that you will understand what is happening. It should run also pretty fast, a good thing for a live migration.

You can consult the South API for more info.

One really important thing, in any method you will use, work on a local copy of your system and database. When you will be really sure that the migration is working well, backup your production DB then apply your migration and then restart your webserver (to load your modified model code).

Etienne
  • 12,440
  • 5
  • 44
  • 50
  • Interesting, thanks for giving me a second option. Sebastjan's method sounds less messy to me, so I'm going to try that one first. I prefer being able to keep the schema and data migrations separate. – Spike Aug 21 '11 at 03:44
  • That's one advantage of my method: you don't migrate any data, you just migrate the schema. But I agree with you that my method is more complicated. – Etienne Aug 21 '11 at 03:49
  • I should add that I was wrong. My method was assuming (erroneously) that there was no good data in your `BaseModel` (ie. no data that need to be migrated to the child models). It's probably not the case. So, if you need to migrate data from the `BaseModel`, between a. and b. you need to add the missing columns in the child models (`db.add_column(table_name, field_name, field)`) and then copy the data (`for row in orm.BaseModel.all():`). – Etienne Aug 23 '11 at 17:19
  • +1 for pointing out how I had to rename the primary_key field in my children models. Turns out I did, and that was a useful thing to be looking for. – Spike Aug 23 '11 at 21:55