Database Migrations
When you are developing your website, you are constantly modifying your database schema: some tables are being added, others are being modified, unneeded tables are being deleted. Managing database schema may become a very complex task, because you need to keep it up-to-date on your development, testing and production servers. You can greatly simplify the task of managing database schema with the concept of migrations.
A migration is some kind of atomic change of state of the database schema. You can apply a migration to upgrade the schema to its newer state, or you can revert a migration to downgrade schema to its previous state. You create new migrations as you develop your website, and you can have as many migrations as you need. All migrations form a database schema change history, which is very convenient, because you know when and for what purpose you changed the schema.
Installing Doctrine\Migrations Component
Doctrine provides a special component Doctrine\Migrations
that you can use
for implementing database migrations in your website.
To install Doctrine\Migrations
component, type the following command:
php composer.phar require doctrine/migrations
The command above will download and install the component files to the vendor
directory
and modify your composer.json
file as follows:
{
...
"require": {
...
"doctrine/migrations": "^1.4",
...
},
...
}
Configuring Migrations
Before you can use migrations, you'll need to provide the configuration describing
what database connection to use, in which table to store migration history, where to
store migration classes, etc. To do that,
add the following lines to your config/autoload/global.php
file:
<?php
return [
'doctrine' => [
// migrations configuration
'migrations_configuration' => [
'orm_default' => [
'directory' => 'data/Migrations',
'name' => 'Doctrine Database Migrations',
'namespace' => 'Migrations',
'table' => 'migrations',
],
],
],
// ...
];
As you can see, we have the doctrine
key and its migrations_configuration
subkey (line 5). Under this subkey we
provide the configuration for migrations:
-
In line 6, we provide the name of entity manager to use (
orm_default
). -
In line 7, we tell Doctrine that we want to store migrations under the
APP_DIR/data/Migrations
directory. -
In line 8, we provide a user-friendly name for our migrations.
-
In line 9, we tell Doctrine that we want that our migration classes live in
Migrations
namespace. You can specify a namespace of your choice. -
In line 10, we tell Doctrine that want to store migration history inside of
migrations
database table. Doctrine will create and manage that table automatically.
Creating Migrations
A migration is a change set upgrading or downgrading the schema to its next or previous state, respectively. You generate a new empty migration with the help of the following commands:
cd APP_DIR
./vendor/bin/doctrine-module migrations:generate
The commands above make the application directory the current working directory and then
run the migrations:generate
console command.
Note
DoctrineModule
and DoctrineORMModule
provide several console commands that you can use for various
database maintenance tasks (like generating or executing migrations). For the list of available commands,
you can use the list
command:
./vendor/bin/doctrine-module list
Once you run the migrations:generate
command, you will be able to find the newly created migration under the APP_DIR/data/Migrations
directory.
The file has a name like VersionYYYYMMDDHHIISS.php
, where YYYY
is current year, MM
is current month, DD
is current day,
HH
, II
and SS
represent current hour, minute and second, respectively.
If you look into the newly created file, you will find the following content:
<?php
namespace Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20160901114333 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
}
}
Note
If you do not see the newly created migration in NetBeans IDE, you need to open the menu Source and select the Scan for external changes menu item.
As you can see from the code above, a migration is a usual PHP class inherited from Doctrine\DBAL\Migrations\AbstractMigration
base class. Every migration should have at least two methods: up()
and down()
. The up()
method upgrades the schema to a newer state,
the down()
method downgrades the schema from its newer state to the previous state. Both up()
and down()
methods have a single
argument of type Doctrine\DBAL\Schema\Schema
, which can be used for actual database schema modifications.
Note
The Schema
class is a part of Doctrine\DBAL
component. For more information about the methods it provides, please
refer to Doctrine DBAL documentation. Another, even better way is to look at the code inside your vendor/doctrine/dbal
directory.
A migration class may optionally have the following (overridden) methods (table 13.1):
Method | Description |
---|---|
isTransactional() |
If this function returns true (default) the migration will be executed in one transaction, |
otherwise non-transactional state will be used to execute each of the migration SQLs. | |
getDescription() |
This method should return a string describing the migration (for what purpose this schema change is done) |
preUp(Schema $schema) |
This method will be executed before upgrading the schema. |
postUp(Schema $schema) |
This method will be executed after upgrading the schema. |
preDown(Schema $schema) |
This method will be executed before downgrading the schema. |
postDown(Schema $schema) |
This method will be executed after downgrading the schema. |
Table 13.1. Methods a migration class may have
The AbstractMigration
base class also provides the following useful methods (table 13.2):
Method | Description |
---|---|
addSql($sql, array $params = [], array $types = []) |
This method allows to execute an arbitrary SQL request. |
write($message) |
This helper method prints a (debug or explanatory) message to screen. |
throwIrreversibleMigrationException($message = null) |
This helper method is typically called inside of down() method to signal that the migration cannot be undone. |
Table 13.2. Methods provided by the base migration class
As you can see from table 13.2, you can also modify the schema by calling addSql()
method. This method
can be used to create a table, to update a table or to remove a table. It can also be used, for example, to insert
some data to a table (however, inserting data is not a schema change).
Note
Doctrine migrations are designed for schema changes, not for inserting data to the database. Although, inserting some initial data to database is useful in some cases.
Now that you know how to create a migration, let's create a couple of migrations for our Blog sample.
Creating the Initial Migration
The first migration we will create is the initial migration. This migration will be applied to empty database
schema and will create four tables: post
, comment
, tag
and post_tag
.
Modify the migration class we have created in the previous section to look like below:
<?php
namespace Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* A migration class. It either upgrades the databases schema (moves it to new state)
* or downgrades it to the previous state.
*/
class Version20160901114333 extends AbstractMigration
{
/**
* Returns the description of this migration.
*/
public function getDescription()
{
$description = 'This is the initial migration which creates blog tables.';
return $description;
}
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// Create 'post' table
$table = $schema->createTable('post');
$table->addColumn('id', 'integer', ['autoincrement'=>true]);
$table->addColumn('title', 'text', ['notnull'=>true]);
$table->addColumn('content', 'text', ['notnull'=>true]);
$table->addColumn('status', 'integer', ['notnull'=>true]);
$table->addColumn('date_created', 'datetime', ['notnull'=>true]);
$table->setPrimaryKey(['id']);
$table->addOption('engine' , 'InnoDB');
// Create 'comment' table
$table = $schema->createTable('comment');
$table->addColumn('id', 'integer', ['autoincrement'=>true]);
$table->addColumn('post_id', 'integer', ['notnull'=>true]);
$table->addColumn('content', 'text', ['notnull'=>true]);
$table->addColumn('author', 'string', ['notnull'=>true, 'lenght'=>128]);
$table->addColumn('date_created', 'datetime', ['notnull'=>true]);
$table->setPrimaryKey(['id']);
$table->addOption('engine' , 'InnoDB');
// Create 'tag' table
$table = $schema->createTable('tag');
$table->addColumn('id', 'integer', ['autoincrement'=>true]);
$table->addColumn('name', 'string', ['notnull'=>true, 'lenght'=>128]);
$table->setPrimaryKey(['id']);
$table->addOption('engine' , 'InnoDB');
// Create 'post_tag' table
$table = $schema->createTable('post_tag');
$table->addColumn('id', 'integer', ['autoincrement'=>true]);
$table->addColumn('post_id', 'integer', ['notnull'=>true]);
$table->addColumn('tag_id', 'integer', ['notnull'=>true]);
$table->setPrimaryKey(['id']);
$table->addOption('engine' , 'InnoDB');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
$schema->dropTable('post_tag');
$schema->dropTable('tag');
$schema->dropTable('comment');
$schema->dropTable('post');
}
}
In the code above we have three methods:
- the
getDescription()
method provides the description of the migration. - the
up()
method upgrades the schema to its newer state by adding new tables. - the
down()
method drops tables thus downgrading the schema to its previous state.
Adding Another Migration
Now assume we decided to improve the performance of our database by adding indexes to our tables.
Note
If you want to learn about database indexes in more details and why indexes are so helpful, you can refer to an excellent tutorial called Use the Index, Luke.
We can also improve data integrity by adding foreign keys. To do this, we have to add another migration. Generate another
empty migration with the migrations:generate
console command. Modify the code to look like below:
<?php
namespace Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* A migration class. It either upgrades the databases schema (moves it to new state)
* or downgrades it to the previous state.
*/
class Version20160901114938 extends AbstractMigration
{
/**
* Returns the description of this migration.
*/
public function getDescription()
{
$description = 'This migration adds indexes and foreign key constraints.';
return $description;
}
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// Add index to post table
$table = $schema->getTable('post');
$table->addIndex(['date_created'], 'date_created_index');
// Add index and foreign key to comment table
$table = $schema->getTable('comment');
$table->addIndex(['post_id'], 'post_id_index');
$table->addForeignKeyConstraint('post', ['post_id'], ['id'], [], 'comment_post_id_fk');
// Add indexes and foreign keys to post_tag table
$table = $schema->getTable('post_tag');
$table->addIndex(['post_id'], 'post_id_index');
$table->addIndex(['tag_id'], 'tag_id_index');
$table->addForeignKeyConstraint('post', ['post_id'], ['id'], [], 'post_tag_post_id_fk');
$table->addForeignKeyConstraint('tag', ['tag_id'], ['id'], [], 'post_tag_tag_id_fk');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
$table = $schema->getTable('post_tag');
$table->removeForeignKey('post_tag_post_id_fk');
$table->removeForeignKey('post_tag_tag_id_fk');
$table->dropIndex('post_id_index');
$table->dropIndex('tag_id_index');
$table = $schema->getTable('comment');
$table->dropIndex('post_id_index');
$table->removeForeignKey('comment_post_id_fk');
$table = $schema->getTable('post');
$table->dropIndex('date_created_index');
}
}
You can find the migrations we have just created inside of the Blog sample bundled with this book.
Executing Migrations
Once our migration classes are ready, you can apply the migrations to database. To do that, you use the
migrations:migrate
console command as follows:
./vendor/bin/doctrine-module migrations:migrate
The command above applies all available migrations in turn. It writes the IDs of the applied migration to
the migrations
database table. After that, the migrations
table will look as follows:
mysql> select * from migrations;
+----------------+
| version |
+----------------+
| 20160901114333 |
| 20160901114938 |
+----------------+
2 rows in set (0.00 sec)
If you want to upgrade or downgrade to some specific version, specify the migration ID as the migrations:migrate
command's argument as follows:
./vendor/bin/doctrine-module migrations:migrate 20160901114333
You can also use 'prev', 'next' and 'first' aliases as version IDs which respectively move database to its previous state, next state or to the state before the first migration (empty database).
So, with migrations you can easily move through migration history and change the database schema as needed. Be careful though that migrations may remove some of your data, so apply them wisely.
Summary
In this chapter, we have learned about database migrations. Doctrine library provides you a component called Doctrine\Migrations
which allows to implement and apply migrations.
A migration is some kind of change set which can be applied to database schema. You can apply a migration to upgrade the schema to its newer state, or you can revert a migration to downgrade schema to its previous state.
Migrations are useful, because they allow to store the history of schema changes and apply changes in a standard way. With migrations, you can easily keep your schema up-to-date on every development machine, and on testing, staging and production servers.