Databázové migrácie s Yii framework? Like a boss

PHPYii

Na poslednom WebElemente som spomínal databázové migrácie ako jednu z najlepších funkcií Yii frameworku.

Takisto si stále stojím za tým, že keď si na ne človek raz zvykne, nebude to chcieť robiť už nikdy inak. Poďme sa teda pozrieť ako to fičí.

Na tomto blogu mi chýbali kategórie. Nie je to nič zložité - jedna tabuľka s kategóriami a jeden nový stĺpec do tabuľky s článkami (jeden článok bude vždy v jednej kategórií).

Po starom by som to robil setom SQL príkazov, ktoré by som potom spustil alebo nejakým jednoduchým PHP skriptom, prípadne akciou v Controlleri. Možností je veľa.

Príkaz "migrate"

Yii framework má integrovaný príkazový riadok, ktorý pomáha pri generovaní kódu (shell), prekladoch (message), vytváraní úvodnej aplikácie (webapp) a databázových migráciách (migrate). Po jeho spustení, bez zadania konkrétneho príkazu dostaneme malú nápovedu.

vlado@pc:/cesta-k-projektu/protected$ ./yiic
Yii command runner (based on Yii v1.1.9)
Usage: ./yiic <command-name> [parameters...]

The following commands are available:
 - message
 - migrate
 - shell
 - webapp

To see individual command help, use the following:
   ./yiic help <command-name>

Nás budú dnes zaujímať len migrácie t.j. príkaz migrate, ktorý spustíme.

vlado@pc:/cesta-k-projektu/protected$ ./yiic migrate

Yii Migration Tool v1.0 (based on Yii v1.1.9)

Creating migration history table "tbl_migration"...done.
No new migration found. Your system is up-to-date.

Systém nás informuje, že vytvoril tabuľku pre migrácie. Tabuľka má nasledovnú štruktúru. Pozrieť si ju môžte napríklad spustením SQL príkazu SHOW CREATE TABLE tbl_migration``.

CREATE TABLE `tbl_migration` (
  `version` varchar(255) NOT NULL,
  `apply_time` int(11) DEFAULT NULL,
  PRIMARY KEY (`version`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1

Pozrime sa na zoznam všetkých dostupných príkazov.

vlado@pc:/cesta-k-projektu/protected$ ./yiic help migrate
USAGE
  yiic migrate [action] [parameter]

DESCRIPTION
  This command provides support for database migrations. The optional
  'action' parameter specifies which specific migration task to perform.
  It can take these values: up, down, to, create, history, new, mark.
  If the 'action' parameter is not given, it defaults to 'up'.
  Each action takes different parameters. Their usage can be found in
  the following examples.

EXAMPLES
 * yiic migrate
   Applies ALL new migrations. This is equivalent to 'yiic migrate up'.

 * yiic migrate create create_user_table
   Creates a new migration named 'create_user_table'.

 * yiic migrate up 3
   Applies the next 3 new migrations.

 * yiic migrate down
   Reverts the last applied migration.

 * yiic migrate down 3
   Reverts the last 3 applied migrations.

 * yiic migrate to 101129_185401
   Migrates up or down to version 101129_185401.

 * yiic migrate mark 101129_185401
   Modifies the migration history up or down to version 101129_185401.
   No actual migration will be performed.

 * yiic migrate history
   Shows all previously applied migration information.

 * yiic migrate history 10
   Shows the last 10 applied migrations.

 * yiic migrate new
   Shows all new migrations.

 * yiic migrate new 10
   Shows the next 10 migrations that have not been applied.

Vytvorím novú migráciu pre kategórie.

vlado@pc:/cesta-k-projektu/protected$ ./yiic migrate create blog_article_categories

Yii Migration Tool v1.0 (based on Yii v1.1.9)

Create new migration '/cesta-k-projektu/protected/migrations/m120501_115118_blog_article_categories.php'? [yes|no] yes
New migration created successfully.

Samozrejme, že som si želal vytvoriť i súbor, do ktorého teraz budem písať príkazy potrebné pre migráciu.

<?php

class m120501_115118_blog_article_categories extends CDbMigration
{
    public function up()
    {
    }

    public function down()
    {
        echo "m120501_115118_blog_article_categories does not support migration down.\n";
        return false;
    }

    /*
    // Use safeUp/safeDown to do migration with transaction
    public function safeUp()
    {
    }
    public function safeDown()
    {
    }
    */
}

Funkcie safeUp a safeDown slúžia na prevedenie potrebných príkazov transakčne. Tými sa pre jednoduchosť teraz zaoberať nebudem.

Migrácia pre novú tabuľku s kategóriami

Skúsim zhrnúť, čo všetko bude treba.

  1. Vytvorenie tabuľky Category a jej úpravy
  2. Pridanie stĺpca do tabuľky Article, kde sú články
  3. Nastavenie cudzieho kľúča

V časti up vytvoríme tabuľku a nastavíme UNIQUE kľuč na stĺpec url. Pridáme stĺpec do tabuľky s článkami a vytvoríme na ňom INDEX. Aby bolo možné nastaviť FOREIGN KEY je nutné všetky články priradiť do nejakej kategórie. Vytvoríme teda kategóriu, prídáme do nej články, a nakoniec nastavíme spomínaný kľúč.

Majte na pamäti, že príkazy v operácii down sa musia prevádzať presne v opačnom poradí (niečo ako Ctrl+Z). Keby som najskôr zmazal tabuľku s kategóriami, tak vďaka ON DELETE CASCADE by boli zmazané i všetky články.

Takže najskôr zmažeme FOREIGN KEY, potom odstránime stĺpec categoryId z tabuľky s článkami, a nakoniec môžeme odstrániť i tabuľku s kategóriami.

Výsledný kód potom vyzerá nasledovne.

<?php

class m120501_115118_blog_article_categories extends CDbMigration
{
    public function up()
    {
        $this->execute("CREATE TABLE `Category` (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` varchar(100) COLLATE 'utf8_general_ci' NOT NULL,
  `url` varchar(100) COLLATE 'utf8_general_ci' NOT NULL
) COMMENT='' ENGINE='InnoDB' COLLATE 'utf8_general_ci';");

        $this->execute("ALTER TABLE `Category`ADD UNIQUE `url` (`url`);");
        $this->execute("ALTER TABLE `Article` ADD `categoryId` int NOT NULL");
        $this->execute("ALTER TABLE `Article` ADD INDEX `categoryId` (`categoryId`);");
        $this->execute("INSERT INTO `Category` (`name`, `url`) VALUES ('Nezaradené', 'nezaradene');");
        $this->execute("UPDATE `Article` SET `categoryId` = (SELECT `id` FROM `Category` WHERE `url` = 'nezaradene' LIMIT 1);");
        $this->execute("ALTER TABLE `Article` ADD FOREIGN KEY (`categoryId`) REFERENCES `Category` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;");

        return TRUE;
    }

    public function down()
    {
        $this->execute("ALTER TABLE `Article` DROP FOREIGN KEY `Article_ibfk_1`;");
        $this->execute("ALTER TABLE `Article` DROP `categoryId`");
        $this->execute("DROP TABLE `Category`;");

        return TRUE;
    }
}

Spustenie migrácie

Migráciu spustíme skriptom yiic s príkazom migrate a parametrom up.

vlado@pc:/cesta-k-projektu/protected$ ./yiic migrate up

Yii Migration Tool v1.0 (based on Yii v1.1.9)

Total 1 new migration to be applied:
    m120501_115118_blog_article_categories

Apply the above migration? [yes|no] yes
*** applying m120501_115118_blog_article_categories
*** applied m120501_115118_blog_article_categories (time: 0.034s)


Migrated up successfully.

Migrácia down.

vlado@pc:/cesta-k-projektu/protected$ ./yiic migrate down

Yii Migration Tool v1.0 (based on Yii v1.1.9)

Total 1 migration to be reverted:
    m120501_115118_blog_article_categories

Revert the above migration? [yes|no] yes
*** reverting m120501_115118_blog_article_categories
*** reverted m120501_115118_blog_article_categories (time: 0.019s)


Migrated down successfully.

Aby ste predišli problémom je fajn si migrácie up aj down otestovať niekoľko krát, aby ste si boli istí, že všeto funguje ako má.

Záver

Toto, samozrejme, nie je jediná možnosť ako pracovať s migráciami. Yii ponúka i veľa metód v triede CDbMigration, ktoré vám umožnia robiť úpravy na databáze jednoduchšie. Spomeniem napr. createTable pre vytvorenie tabuľky, createIndex pre vytvorenie indexu, delete pre príkaz zmazanie, insert pre vloženie, addForeignKey pre cudzie kľúče a podobne.

Čo sa ešte dá?

  • vypnúť interaktívny mód
  • nastaviť vlastnú šablónu pre súbor s migráciou
  • zmeniť tabuľku, kam sa migrácie ukladajú

Pracujete s frameworkom, ktorý vie migrácie? Podeľte sa!