How To: Simple database migrations with Phing and DbDeploy

Introduction

This How To will introduce some simple database migrations to your PHP application. Ruby on Rails is a popular web application framework, that provides a method of migrating (upgrading) the applications database programatically, keeping the database schema essentially version controlled. This allows individual developers to update their working databases and the databases on testing, staging or production machines to be updated with new versions of applications. The CakePHP framework has recently developed a migrations library simliar to rails, but this article focuses on using seperate tools to run database migrations, a build tool called Phing, along with a method for creating database migrations, dbdeploy.

Install Phing

I always use the beta or release candidate of phing and for the purposes of this article I suggest you do too. The best way to download and install phing is using PEAR. This can be done on Linux or Windows assuming you have the pear script in your PATH with three shell commands.

shell> pear channel-discover pear.phing.info
shell> pear config-set preferred_state beta
shell> pear install phing/phing

Example Application structure

As an example, we’re going to develop a simple application with the following directory structure.

example/
 |-- db/
 |   `-- deltas/
 |-- deploy/
 |   `-- scripts/
 |-- library/
 `-- public/

The db directory contains sql files for using and manipulating our database and
the deploy directory contains our build scripts that set the migrations in motion. The library directory contains our application code and the public folder will contain scripts and files accessible directly from the web, but will not be the focus of this article.

Build scripts

This section shows you how to develop the build scripts that will run the database migrations. The first file we need to create is a simple configuration file and should be fairly self explanatory. The file is written as key=value, lines beginning with a # are comments. Open your editor and save the following text as deploy/build.properties.

# Property files contain key/value pairs
#key=value

# This dir must contain the local application
build.dir=../

# Credentials for the database migrations
db.host=localhost
db.user=user
db.pass=password
db.name=example

# paths to programs
progs.mysql=/usr/bin/mysql

The next file we are going to create is the deploy/build.xml file. This is the file that tells Phing what we want it to do. I’m not going to go into too much detail describing each part of the build file, there are some comments, but you should consult the Phing Documentation for further details and enhancements.

<?xml version="1.0" ?>
<project name="PurpleMonkey" basedir="." default="build">

    <!-- Sets the DSTAMP, TSTAMP and TODAY properties -->
    <tstamp/>

    <!-- Load our configuration -->
    <property file="./build.properties" />

    <!-- create our migration task -->
    <target name="migrate" description="Database Migrations">  

        <!-- load the dbdeploy task -->
        <taskdef name="dbdeploy" classname="phing.tasks.ext.dbdeploy.DbDeployTask"/>

        <!-- these two filenames will contain the generated SQL to do the deploy and roll it back-->
        <property name="build.dbdeploy.deployfile" value="deploy/scripts/deploy-${DSTAMP}${TSTAMP}.sql" />
        <property name="build.dbdeploy.undofile" value="deploy/scripts/undo-${DSTAMP}${TSTAMP}.sql" />

        <!-- generate the deployment scripts -->
        <dbdeploy
            url="mysql:host=${db.host};dbname=${db.name}"
            userid="${db.user}"
            password="${db.pass}"
            dir="${build.dir}/db/deltas"
            outputfile="${build.dir}/${build.dbdeploy.deployfile}"
            undooutputfile="${build.dir}/${build.dbdeploy.undofile}" />

        <!-- execute the SQL - Use mysql command line to avoid trouble with large files or many statements and PDO -->
        <exec
            command="${progs.mysql} -h${db.host} -u${db.user} -p${db.pass} ${db.name} &lt; ${build.dbdeploy.deployfile}"
            dir="${build.dir}"
            checkreturn="true" />
    </target>
</project>

That’s essentially all the magic we need. Now we just need to create our database.

Writing dbdeploy delta scripts

We haven’t actually created our database, so rather than create it the traditional way, we will actually use the migrations to create the initial schema. We’ve not actually decided what our example application does yet, but seeing as most tutorials make blogs, why don’t we give that a bash. We’ll start simple, one table with three columns called post.

Field Type Comment
title VARCHAR(255) The title of our post
time_created DATETIME The time we created our post
content MEDIUMTEXT The content of our post

Dbdeploy works by creating numbered delta files. Each delta files contains simple SQL to both deploy the change and roll it back. The basic layout of a delta file is like so.

--//

-- Run SQL to do the changes

--//@UNDO

-- RUN SQL to undo the changes

--//

We are creating our initial schema, so put the following content in db/deltas/1-create_initial_schema.sql

--//

CREATE TABLE `post` (
    `title` VARCHAR(255),
    `time_created` DATETIME,
    `content` MEDIUMTEXT
);

--//@UNDO

DROP TABLE `post`;

--//

Migrating the database

We are one step away from running our first migration. To keep track of the current version of the database, dbdeploy requires a table in the database. This is the only time we will have to interact with the mysql client directly.

shell> mysql -hlocalhost -uroot -ppassword example
mysql> CREATE TABLE changelog (
  change_number BIGINT NOT NULL,
  delta_set VARCHAR(10) NOT NULL,
  start_dt TIMESTAMP NOT NULL,
  complete_dt TIMESTAMP NULL,
  applied_by VARCHAR(100) NOT NULL,
  description VARCHAR(500) NOT NULL
);
mysql> ALTER TABLE changelog ADD CONSTRAINT Pkchangelog PRIMARY KEY (change_number, delta_set);

We are now ready to run our first migration and create the initial schema for our application.

shell>cd deploy
shell>phing migrate

All being well, we now have a posts table in our database. But what about an author for our blog posts? We’ll have to add another table and a foreign key from the post table to author table. To do this we create another delta, we call this one db/deltas/2-create_author_and_link_to_post.sql

--//

CREATE TABLE `author` (
    `author_id` INT(10) unsigned auto_increment,
    `name` VARCHAR(255),
    PRIMARY KEY (`author_id`)
);

ALTER TABLE `post` ADD `author_id` INT(10) unsigned NULL;

--//@UNDO

ALTER TABLE `post` DROP `author_id`;

DROP TABLE `author`;

--//

Run our migrations again.

shell> cd deploy
shell> phing migrate

Conclusion

That’s pretty much it, you’ve seen how to create database deltas and use them to migrate your database, if you can’t be bothered to copy and paste things to try for yourself, download the example application.

There are plenty of caveats when it comes to version controlling databases, especially if you branch and merge your application code, some are detailed in the dbdeploy documentation

This tutorial is probably incomplete or wrong in plenty of ways, if you think you have something to point out, please leave your comments below

  • Digg
  • del.icio.us
  • NewsVine
  • Reddit
  • Furl
  • DZone
  • StumbleUpon
  • Technorati

Tags: , , , , , ,

24 Responses to “How To: Simple database migrations with Phing and DbDeploy”

  1. dbdeploy » PHP and dbdeploy Says:

    [...] Marshal as a writeup on using dbdeploy with Phing on his blog. It’s well worth a [...]

  2. PHP Weekly Reader - April 20th 2008 : phpaddiction Says:

    [...] I use phing for automating my build process so I was very interested in these two articles. PHPUnit test and Code Coverage Statistics with phing and Simple Database migrations with phing and dbdeploy. [...]

  3. 10 tools for Modern PHP Development Says:

    [...] Phing is a project build tool and is a PHP port of the popular Java program ant. Phing can be used to automate builds, database migration, deployment and configuration of code. Documentation. Database migrations with phing [...]

  4. Tower Of Power » Blog Archive » Automating the Development Workflow Says:

    [...] Lets start with build.xml. When you run phing in the command line without specifying the build file, it looks for a file named build.xml by default. Lets see an example build.xml file: The DBDeploy code comes with much thanks to Dave Marshall’s post [...]

  5. Phing - PHP project build system based on Apache Ant « PHP::Impact ( [str blog] ) Says:

    [...] Simple database migrations with Phing and DbDeploy [...]

  6. Extract and compare database schemas « PHP::Impact ( [str blog] ) Says:

    [...] Simple database migrations with Phing and DbDeploy Posted by phpimpact Filed in Databases, Frameworks, PHP, Programming [...]

  7. Managing and applying database changes with LiquiBase « PHP::Impact ( [str blog] ) Says:

    [...] Simple database migrations with Phing and DbDeploy Posted by phpimpact Filed in Databases, PHP, Programming, Tools [...]

  8. Deployment: Porting Shell Scripts to PHP « PHP::Impact ( [str blog] ) Says:

    [...] with an optional task for making revisions to a database called DbDeployTask. Dave Marshall wrote a nice article about it. After testing this task, I came to the conclusion that the technique used is not very effective if [...]

  9. Net Perspective’s Blog » Blog Archive » Time is Money: Save Time With Automation And Phing Says:

    [...] the rsync over ssh and one-line ssh techinques above you can do more complicated tasks like DBDeploy on a live server that denies external access to its databases (PROTIP: rsync the db folder, [...]

  10. Marlin Forbes Says:

    Your setup SQL has an error – you have a comma after the content MEDIUMTEXT column, which shouldn’t be there.

    Also, do you have any idea how to instruct dbdeploy to undo a migration? I cannot find any documentation on how to do that, which sort of makes the library pointless.

  11. daveyboy Says:

    Thanks for the heads up Marlin, will fix it.

    DbDeploy itself doesn’t actual do(or undo) the migrations, it simply generates SQL scripts to do so. Whenever you generate a migration script, it also generates the undo script, but I don’t think there’s functionality to generate the undo from version 10 to version 6 for example.

  12. Evgeniy Kashchenko Says:

    Dave, does Phing support roll-backs for deployments/tasks that will use the “build.dbdeploy.undofile” property you defined?
    Or is it necessary to write another task called “roolback” that will run SQL from undofile?

    PS: Seems like wrong permissions for example application (403).

  13. daveyboy Says:

    @Evgeniy

    You have to write another task.

    When we deploy code to our servers, we generally check out a new version of the code and symlink to it (capistrano style), then migrate the database. Our migrate task places the build.dbdeploy.undofile in the current folder with the code, so effectively I can rollback each deployment we do.

    For developers, we just use the file manually.

    Thanks for the heads up, permissions fixed.

    I’m hoping to write a more in depth article, including rolling back and also incorporating a way of maintaining different codelines, just need to find the time :)

  14. Kevin Says:

    Not sure which version of Phing you were using when writing this post, but i had trouble using your sample delta files. The resulting deployXXX.sql and undoXXX.sql files were truncated sql impossible to execute, and looking into the DbDeployTask.php class i realised that the “undo” comment was missing a . Here what i shopuld be “– //@UNDO” (instead of –//@UNDO).

    Also – and this might be due to different versions of mysql – the “–//” caused the sql not to work, so i just removed them.

    Here the resulting delta (only pasting the first one):

    # CREATE TABLE `post` (
    # `title` VARCHAR(255),
    # `time_created` DATETIME,
    # `content` MEDIUMTEXT
    # );
    #
    # — //@UNDO
    #
    # DROP TABLE `post`;

  15. Kevin Says:

    Dave,

    have you been creating any new useful task in your build.xml file? Like an undo task? :)

    Also, i wanted to know if you were still satisfied with this dbdeploy task almost a year after writing this post?

    And finally, what sort of data do you handle with it? Because i’m about to introduce dbdeploy to my team, but i would like to keep DML (data) and DLL (structure) changes separated so i can have for instance, dev-only deltas, and dev+prod deltas and i’m not sure if there is a best practice for these issues. I’m thinking of using 2 separate directories which would both have a corresponding “deltaSet” (the use of this field is not well documented anywhere though). Any comment/idea would be more than welcome! ;)

  16. daveyboy Says:

    @Kevin

    Regarding the space between the dash and the slashes, we learned this the hard way about two months ago! The sql standard says that there should be a space after the double dash, somebody reported it as a bug and gave the phing guys a patch, I think it came in 2.3.2?

    See: http://phing.info/trac/ticket/288

    —–

    I’m still very happy with phing, even for some very simple tasks, it’s nice and neat.

    Some of our tasks:

    We have a task that puts our app in and out of maintenance mode.

    We have a task to copy our dojo folder to dojo-src, then run the dojo build scripts to create a compact version for quicker downloads
    config

    We have a example config file with placeholders, a task then takes the config file and uses the reflexive task to set the values we need.

    We fetch our code from subversion, placing it in a release folder and setting symlinks to point to the file data, similar to the way capistrano does things.

    PHPDoc, PHPUnit and PHP-Codesniffer run tasks.

    —–

    For data we tend to only include very base data in the deltas, such as standing lists like job titles.

    We’re setup ready to use a second delta set, in the event a maintenance branch needs a delta and the trunk has already created new deltas, but we haven’t done so yet. I’ve tested it and it seems to work well enough, but I wont be 100% till I’ve pushed something into our app with it.

    Having the seperate changesets should work well enough for you.

    /ddl/01-create-person-table.sql
    /dml/01-add-admin-user.sql
    /dml-dev/01-add-test-persons.sql

    Your production box just doesn’t run the dml-dev folder. I like the idea too, you could really get a nice test dataset in the dml-dev folder, ready for developers to crack on with what they should be doing. We actually have a data masking program, that we run on a copy of the production database to generate test data. It seems to work pretty well, and we get realistic enough data, just with a few strange names ;)

    I *will* get round to writing an updated post on phing!

  17. Paul Edenburg Says:

    Using the example provided, the SQL statements never got parsed. The correct example SQL should be:

    – //@DEPLOY

    CREATE TABLE `post` (
    `title` VARCHAR(255),
    `time_created` DATETIME,
    `content` MEDIUMTEXT
    );

    – //@UNDO

    DROP TABLE `post`;

    – //

  18. Paul Edenburg Says:

    In my last reply, there should be two ‘-’ signs in the example SQL, but I guess this is filtered out in one way or the other…

  19. buck Says:

    Thank you, I missed the @DEPLOY in my first attempts.

  20. Norm Katz Says:

    Can DBDeploy pass the schema name (database name) that it is working on to the db update scripts that it’s applying? For mysql, if you want to check if a column exists before adding one to a table you need to query the INFORMATION_SCHEMAS table and filter for the current database in use. Thanks!

    SELECT * FROM information_schema.COLUMNS
    WHERE COLUMN_NAME=’colname’ AND TABLE_NAME=’tablename’ AND TABLE_SCHEMA=’dbname’

  21. davedevelopment Says:

    @Norm

    The standard doens’t specify that at the moment, but it would lose some of it’s benefits if it did. Having executable SQL in the deploy file is good news.

  22. Garbago Bronft Says:

    this is a good example of what is going wrong in the so called “php scene” – totally primitive examples, only the first thought, second thought and consequences NOT even mentioned, imitating good already existing solutions in a bad and primitive way, AND still providing errors in so little and primitive examples – on top of it all this by a so called “certified zend engeneer”… what you do here is bad propaganda on the lowest level – do you understand, that your posting is like a parody of itself?

  23. davedevelopment Says:

    Sorry ;)

  24. Ahmed Nuaman — Freelance Designer and Developer — Blog — Developing And Deploying PHP Apps With Capistrano, Phing And DBDeploy Says:

    [...] Here’s a good tutorial you need to read on how to get you started: http://www.davedevelopment.co.uk/2008/04/14/how-to-simple-database-migrations-with-phing-and-dbdeplo... [...]

Leave a Reply