Question

Let's say that I have a Django app called animals, which contains a model called Cat that is already populated with rows in the database:

class Cat(models.Model):
    objects = models.Manager() 

    name = models.CharField(max_length=255)
    miaow_factor = models.IntegerField()

What is the best way of creating a superclass (e.g. Feline) of Cat, and adding it to the database using South migrations? i.e. I would like to end up with the structure:

class Feline(models.Model):
    objects = models.Manager() 

class Cat(Feline):
    #n.b. Cat now inherits from Feline, not models.Model
    objects = models.Manager() 

    feline = models.OneToOneField(Feline, parent_link = True)
    name = models.CharField(max_length=255)
    miaow_factor = models.IntegerField()

Bear in mind that:

  • Cat.feline should be the new primary key of Cat, replacing Cat.id

  • The values of Cat.id and Cat.feline should be the same, for all Cat objects (so that all ForeignKeys to Cat remain valid)

  • New Cat or Feline objects should have IDs allocated after the largest Cat ID, otherwise you will eventually end up trying to allocate an already used ID to a new Cat or Feline.

  • Any database views that depend on Cat.id should not be deleted (so you cannot delete Cat.id, because that would cause a cascade delete of those views)

Having worked on this problem and the above issues for the last few days, I have a solution that involves (amongst other things) renaming Cat.id to Cat.feline_id that I will give as an answer below - any other alternatives welcome.

Was it helpful?

Solution

My solution works as follows. First, add the Feline model, but leave the Cat model unchanged:

class Feline(models.Model):
    objects = models.Manager() 

class Cat(models.Model):
    objects = models.Manager() 

    name = models.CharField(max_length=255)
    miaow_factor = models.IntegerField()

Next, create and run a schemamigration (manage.py schemamigration animals --auto), in order to add the Feline model to the database.

Next, create a datamigration, (manage.py datamigration animals cat_feline). In the datamigration, add code to create a Feline for each Cat, such that each Feline that is created shares an ID with a Cat. Additionally, change the sequence for new Felines so that all new Felines are allocated IDs that are larger than the largest current Cat ID.

class Migration(DataMigration):    
    def forwards(self, orm):
        #create a Feline for each Cat
        for c in orm['Cat'].objects.all():
            f = orm['Feline']()
            f.id = c.id
            f.save()

        if orm['Feline'].objects.count():
            #if there are any Feline objects, make sure that new ids are allocated after the largest current ID
            last_id = orm['Feline'].objects.latest('id').id
            db.execute('alter sequence animals_feline_id_seq restart with %s;' % (last_id + 1)) 


    def backwards(self, orm):
        #no need to do anything if migrating backwards
        pass 

Next, change the models file to make Cat inherit from Feline, and add the OneToOneField to Cat which will be the new primary key for Cats:

class Feline(models.Model):
    objects = models.Manager() 

class Cat(Feline):
    #n.b. Cat now inherits from Feline, not models.Model
    objects = models.Manager() 

    feline = models.OneToOneField(Feline, parent_link = True)
    name = models.CharField(max_length=255)
    miaow_factor = models.IntegerField()

Next, create another schemamigration, in order to apply these changes to the database. However, don't run the migration. Instead, change the code in the migration to rename the Cat.id column to Cat.feline_id

class Migration(SchemaMigration):

    def forwards(self, orm):
        #original changes generated by South:
        # Deleting field 'Cat.id'
        #db.delete_column(u'animals_cat', u'id')

        # Adding field 'Cat.feline'
        #db.add_column(u'animals_cat', 'feline',
        #              self.gf('django.db.models.fields.related.OneToOneField')(default=None, to=orm['animals.Feline'], unique=True, primary_key=True),
        #              keep_default=False)

        #instead of doing the above, just rename Cat.id to Cat.feline_id
        #and drop the default numbering sequence for the Cat.feline_id field 
        db.rename_column('animals_cat', 'id', 'feline_id')
        db.execute("ALTER TABLE animals_cat ALTER COLUMN feline_id DROP DEFAULT")



    def backwards(self, orm):
        #original changes generated by South:
        # Adding field 'Cat.id'
        #db.add_column('animals_cat', u'id',
        #              self.gf('django.db.models.fields.AutoField')(default=None, primary_key=True),
        #              keep_default=False)

        # Deleting field 'Cat.feline_id'
        #db.delete_column(u'animals_cat', 'feline_id')

        #instead of doing the above, rename Cat.feline_id to Cat.id
        #and reinstate the default numbering sequence for the Cat.id field
        db.rename_column('animals_cat', 'feline_id', 'id')
        db.execute("ALTER TABLE animals_cat ALTER COLUMN id SET DEFAULT nextval('u'animals_cat_id_seq'::regclass)")

        if orm['Cat'].objects.count():
            #if there are any Cat objects, make sure that new ids are allocated after the largest current ID
            last_id = orm['Cat'].objects.latest('id').id
            db.execute('alter sequence animals_cat_id_seq restart with %s;' % (last_id + 1)) 

Finally, run the schemamigration that you have just edited, and you're done.

Now, if you wish, you can easily move some fields (e.g. name) from Cat to Feline, using further schemamigrations and datamigrations.

A further challenge (that I have not had to deal with, luckily) would be if you wanted to create a superclass for multiple existing models - in that case you may not be able to keep the same ID for all instances, as some instances in the two different subclasses might clash on ID. Any thoughts on how to work around this would be welcome.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top