Schema- and data-migration in Django with South

Documentation and tutorials available at http://south.aeracode.org/.
Task: User profile application.
project/requirements.txt:
django==1.4.3
south==0.7.6
Create project, add users application, add users and south to INSTALLED_APPS, configure DATABASES.
python manage.py syncdb
Create initial migrations (once):
python manage.py schemamigration users --initial
Creating migrations directory at '.../project/users/migrations'...
Creating __init__.py in '../project/users/migrations'...
Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate users
Create initial migrations for each app You created.
Create the model:
from django.db import models
class UserProfile(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()
address = models.CharField(max_length=200)
We need to create a new table for this model, schema-migration:
python manage.py schemamigration users --auto
+ Added model users.UserProfile
Created 0002_auto__add_userprofile.py. You can now apply this migration with: ./manage.py migrate users
Don't forget to add 0002_auto__add_userprofile.py to git index, execute migrations:
python manage.py migrate users
Running migrations for users:
- Migrating forwards to 0002_auto__add_userprofile.
> users:0001_initial
> users:0002_auto__add_userprofile
- Loading initial data for users.
Installed 0 object(s) from 0 fixture(s)
What if requirements were updated and we need to store phone number additionally?
Add phone field to the model:
class UserProfile(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()
address = models.CharField(max_length=200)
phone = models.CharField(max_length=20, blank=True, null=True)
Create schema-migration and execute it:
python manage.py schemamigration users --auto
+ Added field phone on users.UserProfile
Created 0003_auto__add_field_userprofile_phone.py. You can now apply this migration with: ./manage.py migrate users
python manage.py migrate users
Running migrations for users:
- Migrating forwards to 0003_auto__add_field_userprofile_phone.
> users:0003_auto__add_field_userprofile_phone
- Loading initial data for users.
Installed 0 object(s) from 0 fixture(s)
Add one more model?
class Country(models.Model):
name = models.CharField(max_length=100)
class UserProfile(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()
country = models.ForeignKey(Country, related_name='country_users', null=True)
address = models.CharField(max_length=200)
phone = models.CharField(max_length=20, blank=True, null=True)
Same as before (schemamigration -> migrate):
python manage.py schemamigration users --auto
+ Added model users.Country
+ Added field country on users.UserProfile
Created 0004_auto__add_country__add_field_userprofile_country.py. You can now apply this migration with: ./manage.py migrate users
python manage.py migrate users
Running migrations for users:
- Migrating forwards to 0004_auto__add_country__add_field_userprofile_country.
> users:0004_auto__add_country__add_field_userprofile_country
- Loading initial data for users.
Installed 0 object(s) from 0 fixture(s)
Increase complexity:
instead name, we need to use fname and lname, and we already have some data in the table.
We can accomplish this with two schema- and one data-migration. In data-migration we need to copy data from name field to fname and lname fields, for example:
(name == 'Mikuru Asahina') -> (fname == 'Mikuru', lname == 'Asahina')
Roadmap:
- add
fnameandlnamefields - copy data from
namefield tofnameandlname - remove
namefield
class UserProfile(models.Model):
name = models.CharField(max_length=200)
fname = models.CharField(max_length=100, blank=True, null=True)
lname = models.CharField(max_length=100, blnak=True, null=True)
email = models.EmailField()
country = models.ForeignKey(Country, related_name='country_users', null=True)
address = models.CharField(max_length=200)
phone = models.CharField(max_length=20, blank=True, null=True)
python manage.py schemamigration users --auto
+ Added field fname on users.UserProfile
+ Added field lname on users.UserProfile
Created 0005_auto__add_field_userprofile_fname__add_field_userprofile_lname.py. You can now apply this migration with: ./manage.py migrate users
python manage.py migrate users
Running migrations for users:
- Migrating forwards to 0005_auto__add_field_userprofile_fname__add_field_userprofile_lname.
> users:0005_auto__add_field_userprofile_fname__add_field_userprofile_lname
- Loading initial data for users.
Installed 0 object(s) from 0 fixture(s)
Create data-migration:
python manage.py datamigration users split_name
Created 0006_split_name.py.
Edit automatically created project/users/migrations/0006_split_name.py file.
class Migration(DataMigration):
def forwards(self, orm):
for up in orm.UserProfile.objects.all():
if not up.name:
continue
names = up.name.split()
if len(names) == 1:
up.fname = names[0]
else:
up.fname = names.pop(0)
up.lname = ' '.join(names)
up.save()
def backwards(self, orm):
for up in orm.UserProfile.objects.all():
up.name = ' '.join((up.fname or '', up.lname or ''))
up.save()
Migration execution looks the same as for schema-migration:
python manage.py migrate users
Running migrations for users:
- Migrating forwards to 0006_split_name.
> users:0006_split_name
- Migration 'users:0006_split_name' is marked for no-dry-run.
- Loading initial data for users.
Installed 0 object(s) from 0 fixture(s)
Finally remove name field:
class UserProfile(models.Model):
fname = models.CharField(max_length=100, blank=True, null=True)
lname = models.CharField(max_length=100, blank=True, null=True)
email = models.EmailField()
country = models.ForeignKey(Country, related_name='country_users', null=True)
address = models.CharField(max_length=200)
phone = models.CharField(max_length=20, blank=True, null=True)
python manage.py schemamigration users --auto
? The field 'UserProfile.name' does not have a default specified, yet is NOT NULL.
? Since you are removing this field, you MUST specify a default
? value to use for existing rows. Would you like to:
? 1. Quit now, and add a default to the field in models.py
? 2. Specify a one-off value to use for existing columns now
? 3. Disable the backwards migration by raising an exception.
? Please select a choice: 2
? Please enter Python code for your one-off default value.
? The datetime module is available, so you can do e.g. datetime.date.today()
>>> 'noname'
- Deleted field name on users.UserProfile
Created 0007_auto__del_field_userprofile_name.py. You can now apply this migration with: ./manage.py migrate users
python manage.py migrate users
Running migrations for users:
- Migrating forwards to 0007_auto__del_field_userprofile_name.
> users:0007_auto__del_field_userprofile_name
- Loading initial data for users.
Installed 0 object(s) from 0 fixture(s)
If you want to use South in project that already has tables in database: read about converting an app.
Links:
Licensed under CC BY-SA 3.0