Database migrations

Dependency on the database is a hard one. Ideally, your application will not depend on the separate relational database management system (RDBMS) at all. But, most of the time, clients need the applications, which are conceptually just the elaborate CRUD interfaces over the database, so this dependency is inevitable.

The troubles caused with collaborative development are well known. If the application expects the database to be of some structure, and in the process of development a change is made to the code base, which changes some of these expectations, we need to correct the database schema on every machine this code base is currently deployed to. Of course, we should also have some sort of database schema initialization script, and this change must be introduced in this script too.

The migrations trick employed by Yii (and described in the documentation at http://www.yiiframework.com/doc-2.0/guide-db-migrations.html) has roots in the Ruby on Rails migrations concept (described in their alien documentation at http://guides.rubyonrails.org/migrations.html). You already saw that the Yii 2 database migration is essentially a small program that can use the methods of the yiidbMigration class to perform changes in the relational database schema in a uniform, vendor-independent way. Using them has obvious benefits over running the raw SQL scripts:

  • You are independent of the database vendor. In fact, you can even swap the underlying database over the application lifetime without any change in the existing migrations collected over the time.
  • You are at the application level when inside a migration. Arbitrary checks can be done before messing with the database. More than that, you actually can run the arbitrary code in the migration, not necessarily database-related at all.

There is an additional non-obvious feature: if you want, you can define both upgrade and downgrade routines. As a result, you will be able to move back and forth between versions of the database. However, this is only if you are very careful with your changes in the database, as some changes can be irreversible. This feature was presented to you in every migration template so far:

class m140318_173202_add_auth_key_to_user extends yiidbMigration
{
    public function up()
    {
        $this->addColumn('user', 'auth_key', 'string UNIQUE'),
    }
    public function down()
    {
        $this->dropColumn('user', 'auth_key'),
    }
}

While the up() method is what you usually want to make the change you need, the down() method is there to rollback changes. As it was said, there exist irreversible changes. For example, you can decide at some point to rehash user passwords using a newer crypt algorithm, and this is really a one-way operation, as you certainly don't want to store the old dataset just to have downgrading support. For such irreversible operations, you can just omit the down() method, and when downgrading, this migration will be silently skipped.

If the up() method returns a false Boolean value, this migration is considered not applied and all of the possible future migrations are canceled. This is useful if you make the changes that require possible manual preparations from the operator. The conditional expression in the migration will check whether the preparations are needed and halt the migration if necessary. Then, after the problem is resolved, the migration will proceed as usual.

The down() method has the same feature. If it returns a false Boolean value, the current downgrade along with all possible next downgrades will be canceled. This can be used in case you have some really irreversible changes in the up() method, after which there's nowhere to revert to. The default template for migrations in Yii 2 prepares exactly this kind of down() method for you.

While downgrading is especially useful for you to be able to easily correct possible mistakes in the up() method, it can be used to revert the state of the application to some specific point of the development history. This means that if someone encounters the bug in the database-dependent code in version x of your application, and the most recent version is, say, x + 5, then you can peek at the commit history in your version control system, notice the latest migration script present there, and roll back the migrations to that point. After that, you revert the code base five versions back using the version control system and will get the state of the application exactly at version x. This is less invasive than rolling the code base back to the version x, destroying the current database, and recreating it from scratch using only the up() methods, but you need pretty serious discipline to always make reversible or harmless irreversible changes in migrations.

Apart from this complication with the downgrade feature, there's absolutely nothing complex in the migrations concept itself. Using them, on the other hand, is a different topic.

We have used migrations extensively through the course of this book already, so you should be accustomed to the ./yii migrate command itself (which is a shorthand to ./yii migrate/up, and in the next section we'll explain why). In addition to this, running the ./yii help migrate command will bring you the list of all possible invocations of ./yii migrate.

To know which migrations are applied to the database and which are not (yet), Yii 2 creates and manages a special table in the database behind your back, the name of which is configured at the yiiconsolecontrollersMigrateController::$migrationTable property. Each record in this table holds the name of the migration class used and the time of its usage. The default name for this table is migration, and if you happen to use this name for one of your own tables, you need to change this setting of MigrateController.

For such a fundamental change, it'll be cumbersome to pass the --migrationTable named parameter to each invocation of ./yii migrate, so it's better to use the application configuration instead. As this is a controller and not a component, you need to use the controllerMap setting of the console application for this, as follows:

'controllerMap' => [
    'migrate' => [
        'class' => 'yiiconsolecontrollersMigrateController',
        'migrationTable' => 'my_custom_migrate_table',
    ],
]

In the same way, you can override any other property of the console controllers. This technique is applicable to web controllers as well.

Tip

Note that this parameter overriding has an important side effect: we essentially declare the ID for the specific controller with preset parameters. Nothing stops us from declaring the same console controller class several times under different IDs and with different settings. For example, with HashController discussed before, we can do the following:

    'controllerMap' => [
        'silentHash' => [
            'class' => 'appcommandsHashController',
            'interactive' => false
        ]
    ]

This enables the ./yii silentHash invocation for us without the need of additional class definition.

Another important property is migrationPath, which is the directory where MigrationController will search for the migration classes. By default, its migrations subdirectory is under the root of code base, and this is exactly what we decided on in Chapter 2, Making a Custom Application with Yii 2 (miraculously). We used this property in Chapter 6, User Authorization and Access Control, when we set up the initial schema for RBAC in the database.

You can also use the db property, which should be either the yiidbConnection instance or the string ID of the application component, that is, the yiidbConnection instance. Using this property, you can run migrations on different databases. By manipulating the migrationPath and db properties inside the controllerMap setting of the console application, you can even manage several different databases inside the same application. By default, it has the value db, which is the ID of the default database connection component in the Yii application.

Finally, there is the templateFile setting, which we'll play with in the next section.

Note

You can read in the documentation for MigrateController that there are two complementary methods named safeUp() and safeDown(), which do what up() and down() do, correspondingly, except they do it in transactions. There is an important catch though. In MySQL, for versions up to 5.5 (at least), you may as well forget about them, as any command from the data definition language will automatically be committed. Thus, the following code will create the first and second tables, even while the transaction should fail because of an exception:

    public function safeUp()
    {
        $this->createTable('first', ['id' => 'pk', 'name' => 'string']);
        $this->createTable('second', ['id' => 'pk', 'value' => 'int']);
        throw new LogicException;
        $this->createTable('third', ['id' => 'pk', 'value' => 'date']);
    }

This is documented behavior, but the importance of this point is maybe not stressed enough. The net result is that you should just not use the safe methods with MySQL at all. More than that, you should never override both the up() and safeUp() methods in the same migration class, as the parent implementation of up() is what calls safeUp() internally. The same is the case for down() and safeDown().

Making custom templates for database migrations

Here is what a default template for database migrations looks like in the Yii 2 framework at the time of writing this chapter:

use yiidbSchema;

class <?= $className ?> extends yiidbMigration
{
    public function up()
    {

    }

    public function down()
    {
        echo "<?= $className ?> cannot be reverted.
";

        return false;
    }
}

Let's pretend that we are a careful and disciplined team of developers, which has excellent documentation on all source code files. We use the capable RDBMS, so transactions is a norm for us (let's not call any names, as flame war is not our intention), and we are pretty strict in our changes to the database schema, so our changes are almost always reversible. In this case, the following template will be more suitable for us:

/**
 * TODO: Migration explanation.
 */
class <?= $className ?> extends yiidbMigration
{
    public function safeUp()
    {
        // TODO: migration routine contents.
    }

    public function safeDown()
    {
        // TODO: migration rollback contents.
    }
}

The changes are as follows:

  • Introduced the DocBlock for the explanation for migration
  • Used the transactional versions of the up() and down() methods
  • Clearly marked the places to be filled with actual code
  • Removed return false from rollback routine, as our migrations will be reversible more often than irreversible

To not bloat the code base with another subdirectory, let's place this template in the views/layouts/migration.php file, as it is the most logical place to use. Do not forget that this file is a PHP script that will be processed by PHP runtime and outputted as any other PHP script. So, in addition to the previous code, the views/layouts/migration.php file should contain the following lines at the top:

<?php
/**
 * Template for migrations.
 * Property named `MigrateController.templateView` controls what template to use.
 */
echo "<?php
";
?>

It's good style to provide explanations for any source code file overall, and we must output the <?php processing directive at the beginning of the resulting file.

Now, it's really simple to wire this template to our application, so all future migrations will be written based on this template. We need to use the controllerMap.migrate.templateFile setting in the console application configuration:

    'controllerMap' => [
        'migrate' => [
            'class' => 'yiiconsolecontrollersMigrateController',
            'templateFile' => '@app/views/layouts/migration.php'
        ]
    ]

Unfortunately, even if Yii automatically enables MigrateController to be available, we still must specify the class setting for the migrate controller ID. Not so hard, though. Note that the templateFile setting can accept the path aliases, which is really useful.

With this preparation in place, all future migrations created will look as described before, quite differently from the standard view. As we already explored before, by defining several different controller IDs in the controllerMap setting, we can configure several different invocations of the MigrateController using different templates, if needed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.68.18