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
fname
andlname
fields - copy data from
name
field tofname
andlname
- remove
name
field
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: