My favorites | Sign in
Project Logo
                
Search
for
Updated Sep 03, 2008 by simonwillisonnet
Labels: tutorial, Featured
DmigrationsTutorial  
The dmigrations tutorial

The dmigrations tutorial

dmigrations requires Django 1.0 (beta or final)

Installation

1. Check out dmigrations somewhere on your Python path:

$ svn co http://dmigrations.googlecode.com/svn/trunk/dmigrations dmigrations

2. Add dmigrations to your project's INSTALLED_APPS setting:

INSTALLED_APPS = (
    ...
    'dmigrations',
)

3. Create a directory to hold your migrations and add it to a new DMIGRATIONS_DIR setting. I like to make this directory relative to the directory that holds my settings.py file so I can move my project about:

$ chdir my_project_dir
$ mkdir migrations

# in settings.py:
import os
DMIGRATIONS_DIR = os.path.join(os.path.dirname(__file__), 'migrations')

# Or you could just do this:
DMIGRATIONS_DIR = 'path/to/your/migrations/'

Tutorial: Creating a simple forum

First, create a new project:

$ django-admin.py startproject simple_forum

And create a 'forum' application within that project, and create the project's migrations directory:

$ cd simple_forum
$ mkdir migrations
$ ./manage.py startapp forum

Next, make the following changes to settings.py:

DATABASE_ENGINE = 'mysql'
DATABASE_NAME = 'simple_forum'

INSTALLED_APPS = (
    'dmigrations',
    'forum',
)

import os
DMIGRATIONS_DIR = os.path.join(os.path.dirname(__file__), 'migrations')

Now create the database:

$ mysql -u root
mysql> create database simple_forum;
Query OK, 1 row affected (0.07 sec)

Creating the tables for an application

Let's set up a simple pair of Topic / Post models.

# forum/models.py
from django.db import models

class Topic(models.Model):
    name = models.CharField(max_length = 255)

class Post(models.Model):
    topic = models.ForeignKey(Topic)
    body = models.TextField()
    posted = models.DateTimeField(auto_now_add = True)

Now we need to create the corresponding database tables. Normally we would run syncdb to do this, but now that we're using dmigrations we run dmigration app instead:

$ ./manage.py dmigration app forum
Created migration: ...simple_forum/migrations/001_forum_models.py

The above command generates our first migration. The file it creates looks like this:

from dmigrations.mysql import migrations as m
import datetime
migration = m.Migration(sql_up=["""
    CREATE TABLE `forum_topic` (
        `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
        `name` varchar(255) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    ;
""", """
    CREATE TABLE `forum_post` (
        `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
        `topic_id` integer NOT NULL,
        `body` longtext NOT NULL,
        `posted` datetime NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    ;
""", """
    ALTER TABLE `forum_post` ADD CONSTRAINT topic_id_refs_id_9b35c97 FOREIGN KEY (`topic_id`) REFERENCES `forum_topic` (`id`);
"""], sql_down=["""
    DROP TABLE `forum_post`;
""", """
    DROP TABLE `forum_topic`;
"""])

As you can see, the migration object has both sql_up and sql_down arguments. sql_up is run when the migration is applied, whereas sql_down is run when it is unapplied (and should revert the changes made when the migration was initially run).

Before we run our migration, let's see a list of all available migrations:

$ ./manage.py dmigrate list
* [ ] 001_forum_models

There's one migration, the one we just created. Let's apply it to our database:

$ ./manage.py dmigrate up
Applying migration 001_forum_models

The up command applies the next unapplied migration. Sure enough, if we list the tables in our database forum_post and forum_topic have been created:

$ mysql -u root simple_forum
mysql> show tables;
+------------------------+
| Tables_in_simple_forum |
+------------------------+
| dmigrations            | 
| dmigrations_log        | 
| forum_post             | 
| forum_topic            | 
+------------------------+
4 rows in set (0.00 sec)

Note that there are two other tables in the database now: dmigrations and dmigrations_log. These are created automatically the first time the migrations system is invoked, and are used to keep track of which migrations have been executed.

Run dmigrate list again to see that the migration has indeed been applied:

$ ./manage.py dmigrate list
* [+] 001_forum_models

Now let's roll back the migration using the down command:

$ ./manage.py dmigrate down
Unapplying migration 001_forum_models
$ mysql -u root simple_forum
mysql> show tables;
+------------------------+
| Tables_in_simple_forum |
+------------------------+
| dmigrations            | 
| dmigrations_log        | 
+------------------------+
2 rows in set (0.00 sec)

We'll run the migration again, but this time using the all command (which executes all unapplied migrations in sequence):

$ ./manage.py dmigrate all
Applying migration 001_forum_models

Adding a table

Let's add a new model for announcements - special messages that appear at the top of our forum. First, add the new model to forum/models.py:

class Announcement(models.Model):
    body = models.TextField()
    posted = models.DateTimeField(auto_now_add = True)

Next, create a migration to add the table. We can do this with the dmigration addtable command, which takes the name of the application followed by the name of the model (in lower case):

$ ./manage.py dmigration addtable forum announcement
Created migration: ...simple_forum/migrations/002_forum_models.py

Now we can run the migration with dmigrate up:

$ ./manage.py dmigrate up
Applying migration 002_forum_models

Run dmigrate list to see the full list of applied migrations again:

$ ./manage.py dmigrate list
* [+] 001_forum_models
* [+] 002_forum_models

Adding columns

So far, our forum is anonymous - we don't store any details about the person who created a post. Let's add fields for their name and their IP address. First, add the field definitions to forum/models.py:

class Post(models.Model):
    ...
    name = models.CharField(max_length = 100)
    ip = models.IPAddressField()

Now we need to generate migrations to add those columns, using the dmigration addcolumn command:

$ ./manage.py dmigration addcolumn forum post name
Created migration: ...simple_forum/migrations/003_add_column_name_to_forum_post.py
$ ./manage.py dmigration addcolumn forum post ip
Created migration: 
...simple_forum/migrations/004_add_column_ip_to_forum_post.py

Combining migrations

We've created two new migrations. We could run them right now using dmigration all, but it would be neater if we could combine them in to one migration, allowing us to make a single atomic change to the database. To do that, we'll have to edit the migration files themselves.

First, open up both files in a text editor. They look like this:

# 003_add_column_name_to_forum_post.py:
from dmigrations.mysql import migrations as m
import datetime
migration = m.AddColumn('forum', 'post', 'name', 'varchar(100) NOT NULL')

# 004_add_column_ip_to_forum_post.py:
from dmigrations.mysql import migrations as m
import datetime
migration = m.AddColumn('forum', 'post', 'ip', 'char(15) NOT NULL')

A migration is simply a Python file that contains a single object called migration. This object must be an instance of a BaseMigration subclass, and must provide up() and down() methods.

We can combine our two migrations in to one using a Compound migration. Edit 003_add_column_name_to_forum_post.py to look like this:

from dmigrations.mysql import migrations as m

add_name = m.AddColumn('forum', 'post', 'name', 'varchar(100) NOT NULL')
add_ip = m.AddColumn('forum', 'post', 'ip', 'char(15) NOT NULL')

migration = m.Compound([add_name, add_ip])

Save that file as 003_add_name_and_ip_to_forum_post.py, and delete the two migrations we just generated.

Now we can run our new compound migration using dmigrate up:

./manage.py dmigrate up
Applying migration 003_add_name_and_ip_to_forum_post

If you look in MySQL, you'll see that the new columns have been added to the table:

$ mysql -u root simple_forum
mysql> describe forum_post;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment | 
| topic_id | int(11)      | NO   | MUL | NULL    |                | 
| body     | longtext     | NO   |     | NULL    |                | 
| posted   | datetime     | NO   |     | NULL    |                | 
| name     | varchar(100) | NO   |     | NULL    |                | 
| ip       | char(15)     | NO   |     | NULL    |                | 
+----------+--------------+------+-----+---------+----------------+
6 rows in set (0.12 sec)

Next steps

dmigrate has a help command; run it to see an overview of commands available to you:

$ ./manage.py dmigrate help
Usage: manage.py dmigrate [options] [command] [arguments]

Commands:
./manage.py dmigrate apply M1 M1 - Apply specified migrations
./manage.py dmigrate unapply M1 M2 - Unapply specified migration
./manage.py dmigrate mark_as_applied M1 M2 - Mark specified migrations as applied without running them
./manage.py dmigrate mark_as_unapplied M1 M2 - Unapply specified migration as unapplied without running them
./manage.py dmigrate cat M1 M2 - Print specified migrations

./manage.py dmigrate all      - Run all migrations
./manage.py dmigrate up       - Apply oldest unapplied migration
./manage.py dmigrate down     - Unapply newest applied migration

./manage.py dmigrate to M     - Apply all migrations up to M, upapply all migrations newer than M
./manage.py dmigrate upto M   - Apply all migrations up to M
./manage.py dmigrate downto M - Upapply all migrations newer than M

./manage.py dmigrate init     - Ensure migration system is initialized

./manage.py dmigrate list     - List all migrations and their state
./manage.py dmigrate help     - Display this message


Options:
  --settings=SETTINGS   The Python path to a settings module, e.g.
                        "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be
                        used.
  --pythonpath=PYTHONPATH
                        A directory to add to the Python path, e.g.
                        "/home/djangoprojects/myproject".
  --traceback           Print traceback on exception
  --dev                 Run development migrations (DEV in the filename) as
                        well
  --nodev               Exclude development migrations (DEV in the filename)
  --print-plan          Only print plan
  --verbosity=VERBOSITY
                        Verbosity level; 0=minimal, 1=normal output, 2=all
                        output
  --version             show program's version number and exit
  -h, --help            show this help message and exit

For more advanced migrations (such as migrations that alter your data) you'll need to write your own migration classes. This is relatively straight forward; take a look at the built-in migrations in dmigrations/mysql/migrations.py for examples of how these should work.


Comment by MrSeanReed, Oct 02, 2008

I get this error after executing dmigrate up on an addtable migration:

Traceback (most recent call last):

File "manage.py", line 11, in <module>
execute_manager(settings)
File "/usr/lib/python2.5/site-packages/django/core/management/init.py", line 340, in execute_manager
utility.execute()
File "/usr/lib/python2.5/site-packages/django/core/management/init.py", line 295, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/lib/python2.5/site-packages/django/core/management/base.py", line 77, in run_from_argv
self.execute(args, options.dict)
File "/usr/lib/python2.5/site-packages/django/core/management/base.py", line 96, in execute
output = self.handle(args, options)
File "/usr/lib/python2.5/site-packages/dmigrations/management/commands/dmigrate.py", line 75, in handle
migration_state.apply(migration_name)
File "/usr/lib/python2.5/site-packages/dmigrations/migration_state.py", line 53, in apply
migration.up()
File "/usr/lib/python2.5/site-packages/dmigrations/mysql/migrations.py", line 23, in up
self.execute_sql(self.sql_up)
File "/usr/lib/python2.5/site-packages/dmigrations/mysql/migrations.py", line 49, in execute_sql
cursor.execute(statement.replace('%', '%%'))
File "/usr/lib/python2.5/site-packages/django/db/backends/util.py", line 19, in execute
return self.cursor.execute(sql, params)
File "/usr/lib/python2.5/site-packages/django/db/backends/mysql/base.py", line 83, in execute
return self.cursor.execute(query, args)
File "/var/lib/python-support/python2.5/MySQLdb/cursors.py", line 166, in execute
self.errorhandler(self, exc, value)
File "/var/lib/python-support/python2.5/MySQLdb/connections.py", line 35, in defaulterrorhandler
raise errorclass, errorvalue
mysql_exceptions.OperationalError?: (1005, "Can't create table './uni_latest/#sql-14e9_2d.frm' (errno: 150)")

Table appears to have been created despite this error. Whats going on?

Comment by bradleyw, Oct 04, 2008

It's not clear from the documentation how one would go about generating the plumbing tables (like users, comments etc.), so this is how you do it:

./manage.py dmigration app auth
./manage.py dmigration app contenttypes
./manage.py dmigration app sessions
./manage.py dmigration app admin
./manage.py dmigration app sites
./manage.py dmigration app comments

For each django.contrib.{name} you have in your installed apps. Obviously it's prudent to do this before you run other migrations, so might be a good feature request to automatically create these tables on the first migration run.

Comment by robertrv, Nov 18, 2008

the problem reported in the comment "MrSeanReed?, Oct 02, 2008" is related to have different engines in your mysql database. I have the same problem and my problem was that the dmigration tries to create a table with innodb engine and with foreign keys to myisams tables.

As myisam doesn't support foreign keys the creation gets an error while trying to activate the foreign keys. Changing the default engine to myisam solves the problem, and the creation was correct but the alter table was incorrect (and innecessary due in myisam fk has no sense) so your table is properly created but the error was innecessary.


Sign in to add a comment
Hosted by Google Code