Adding a "through" table to django field and migrating with South?
Asked Answered
M

3

16

Seems like this should be "easy" or at least documented somewhere, I just cant find it.

Lets say I have a model:

class A(models.Model):
    users = models.ManyToMany('auth.User', blank=True)

Now I want to migrate to have a through table to add fields to the ManyToMany relation...

class AUsers(models.Model):
    user = models.ForeignKey('auth.User')
    a = models.ForeignKey('A')
    new_field = models.BooleanField()

class A(models.Model):
    users = models.ManyToMany('auth.User', blank=True, through='AUsers')

Then I do:

% ./manage.py schemamigration app --auto

Not totally surprising, it tells me it is going to drop the original auto-created through table and create a new one for AUsers. What's the best practice at this point? Is there a decent way to migrate to the new through table? Do I use db_table in Meta? Do I just not use the through=... right away... then do a schemamigration --auto, then a datamigration to copy the current table (somehow, not sure...) and then add the through relation and let it kill the table?

What's the trick here? Is this really that hard?

Margrettmarguerie answered 19/5, 2011 at 18:40 Comment(0)
O
15

You should be able to do this pretty easily.

First of all, make sure that the manual through table that you are creating has the same table name in the database as the one Django originally created automatically.

So, first, let's consider a manual through model before your change:

class AUsers(models.Model):
    user = models.ForeignKey('auth.User')
    a = models.ForeignKey('A')

    class Meta:
        db_table = 'appname_a_user'

That should be functionally (almost) identical to the ManyToManyField you used to have. Actually, you could make an empty migration and apply it, and then use --auto for your changes (but don't).

Now, add your field like you did in your sample code above, and then run ./manage.py schemamigration appname manual_through_table --empty. That will give you an empty migration named ####_manual_through_table.py.

In the migration itself, there will be a forwards and backwards method. Each one needs to be one line each:

def forwards(self, orm):
    db.add_column('appname_a_user', 'new_field', self.gf('django.db.models.fields.BooleanField')(default=False))

def backwards(self, orm):
    db.delete_column('appname_a_user', 'new_field')

That should get you what you are after.

Oquendo answered 19/5, 2011 at 19:10 Comment(3)
Yeah, I was kind of wondering if this was a required manual migration. It appears that since you think so too... that that is correct. The db_table is also something I suppose I'll have to do too. Just seems like a piece of "cruft" that only exists because of its migration history and not necessary... but I suppose that's what I'll do. I'll mark accept if/when it works.Margrettmarguerie
You need to add a unique_constraint too in order for the migration to be "complete". I feel like this was a little too complicated... at least more than it needed to be? I'll leave this open for a few days and see if someone has a better solution... I'd like to see one... Maybe I need to hack on South to add it in for the future?Margrettmarguerie
@Margrettmarguerie just run into the same scenario myself. You can (now, i don't know about back then) use db.rename_table to change the table name to whatever you want.Blest
P
8

If anyone comes across this question when trying to do the same thing with the moderns migration framework, here are the steps:

  1. Create a new model class that exactly matches the built-in through table
  2. Use the Meta class to set the table name to match the existing table
  3. Generate a migration, which will create the new table and set it as the through for the field.
  4. Without running that migration, edit it to wrap it in a migrations. SeparateDatabaseAndState migration, where the auto-generated steps are in the state_operations field and the database operations are empty.
  5. Modify your through table, as required, making sure to generate new migrations as normal.
Palmapalmaceous answered 17/2, 2016 at 16:59 Comment(1)
editing to add an example failed miserably... here is a link with an example to avoid confusionHemorrhoid
L
0

As mentioned in a comment, the first step may be simplified using db.rename_table as described here, which gives this through model:

class AUsers(models.Model):
user = models.ForeignKey('auth.User')
a = models.ForeignKey('A')

class Meta:
    unique_together = (('user', 'a'),)

Then, create a migration with --auto (this way you'll have the names of the DB tables visible), and replace the content with:

class Migration(SchemaMigration):

    def forwards(self, orm):
        db.rename_table('appname_a_user', 'appname_auser')

    def backwards(self, orm):
        db.rename_table('appname_auser','appname_a_user') 

I just applied it in my project without issues.

Legaspi answered 5/11, 2015 at 12:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.