Migrações de Dados (Migrations)

Durante o curso de desenvolvimento e manutenção de uma aplicação orientada a banco de dados, a estrutura de banco sendo usada evolui ao mesmo tempo em que o código. Por exemplo, durante o desenvolvimento de uma aplicação, a criação de uma nova tabela pode ser necessária; após ser feito o deploy da aplicação em produção, pode ser descoberto que um índice deveria ser criado para melhorar a performance de alguma query; entre outros. Como a mudança de uma estrutura de banco de dados normalmente necessita de alguma mudança no código, o Yii suporta a então chamada funcionalidade de migração de dados que permite que você mantenha um registro das mudanças feitas no banco de dados em termos de migrações de dados que são versionadas em conjunto com o código fonte da aplicação.

Os seguintes passos mostram como uma migração de dados pode ser usada pela equipe durante o desenvolvimento:

  1. João cria uma nova migração (ex. cria uma nova tabela, muda a definição de uma coluna, etc.).
  2. João comita a nova migração no sistema de controle de versão (ex. Git, Mercurial).
  3. Pedro atualiza seu repositório a partir do sistema de controle de versão e recebe a nova migração.
  4. Pedro aplica a nova migração ao seu banco de dados local em seu ambiente de desenvolvimento, e assim, sincronizando seu banco de dados para refletir as mudanças que João fez.

E os seguintes passos mostram como fazer o deploy para produção de uma nova versão:

  1. Luiz cria uma nova tag para o repositório do projeto que contem algumas novas migrações de dados.
  2. Luiz atualiza o código fonte no servidor em produção para a tag criada.
  3. Luiz aplica todas as migrações de dados acumuladas para o banco de dados em produção.

O Yii oferece um conjunto de ferramentas de linha de comando que permitem que você:

  • crie novas migrações;
  • aplique migrações;
  • reverta migrações;
  • reaplique migrações;
  • exiba um histórico das migrações;

Todas estas ferramentas são acessíveis através do comando yii migrate. Nesta seção nós iremos descrever em detalhes como realizar várias tarefas usando estas ferramentas. Você também pode descobrir como usar cada ferramenta através do comando de ajuda yii help migrate.

Observação: os migrations (migrações) podem afetar não só o esquema do banco de dados, mas também ajustar os dados existentes para se conformar ao novo esquema, como criar novas hierarquias de RBAC ou limpar dados de cache.

Criando Migrações

Para criar uma nova migração, execute o seguinte comando:

yii migrate/create <nome>

O argumento obrigatório nome serve como uma breve descrição sobre a nova migração. Por exemplo, se a migração é sobre a criação de uma nova tabela chamada noticias, você pode usar o nome criar_tabela_noticias e executar o seguinte comando:

yii migrate/create criar_tabela_noticias

Observação: Como o argumento nome será usado como parte do nome da classe de migração gerada, este deve conter apenas letras, dígitos, e/ou underline.

O comando acima criará um novo arquivo contendo uma classe PHP chamada m150101_185401_criar_tabela_noticias.php na pasta @app/migrations. O arquivo contém o seguinte código que declara a classe de migração m150101_185401_criar_tabela_noticias com o código esqueleto:

<?php

use yii\db\Schema;
use yii\db\Migration;

class m150101_185401_criar_tabela_noticias extends Migration
{
    public function up()
    {
    }

    public function down()
    {
        echo "m101129_185401_criar_tabela_noticias cannot be reverted.\n";
        return false;
    }
}

Cada migração de dados é definida como uma classe PHP estendida de yii\db\Migration. O nome da classe de migração é automaticamente gerado no formato m<YYMMDD_HHMMSS>_<Nome>, onde

  • <YYMMDD_HHMMSS> refere-se a data UTC em que o comando de criação da migração foi executado.
  • <Nome> é igual ao valor do argumento nome que você passou no comando.

Na classe de migração, é esperado que você escreva no método up() as mudanças a serem feitas na estrutura do banco de dados. Você também pode escrever códigos no método down() para reverter as mudanças feitas por up(). O método up() é invocado quando você atualiza o seu banco de dados com esta migração, enquanto o método down() é invocado quando você reverte as mudanças no banco. O seguinte código mostra como você pode implementar a classe de migração para criar a tabela noticias:


use yii\db\Schema;
use yii\db\Migration;

class m150101_185401_criar_tabela_noticias extends \yii\db\Migration
{
    public function up()
    {
        $this->createTable('noticias', [
            'id' => Schema::TYPE_PK,
            'titulo' => Schema::TYPE_STRING . ' NOT NULL',
            'conteudo' => Schema::TYPE_TEXT,
        ]);
    }

    public function down()
    {
        $this->dropTable('noticias');
    }

}

Observação: Nem todas as migrações são reversíveis. Por exemplo, se o método up() deleta um registro de uma tabela, você possivelmente mente não será capaz de recuperar este registro com o método down(). Em alguns casos, você pode ter tido muita preguiça e não ter implementado o método down(), porque não é muito comum reverter migrações de dados. Neste caso, você deve retornar false no método down() para indicar que a migração não é reversível.

A classe base yii\db\Migration expõe a conexão ao banco através da propriedade db. Você pode usá-la para manipular o esquema do banco de dados usando os métodos como descritos em Trabalhando com um Esquema de Banco de Dados.

Ao invés de usar tipos físicos, ao criar uma tabela ou coluna, você deve usar tipos abstratos para que suas migrações sejam independentes do SGBD. A classe yii\db\Schema define uma gama de constantes para representar os tipos abstratos suportados. Estas constantes são nomeadas no formato TYPE_<NOME>. Por exemplo, TYPE_PK refere-se ao tipo chave primária auto incrementável; TYPE_STRING refere-se ao típo string. Quando a migração for aplicada a um banco de dados em particular, os tipos abstratos serão traduzidos nos respectivos tipos físicos. No caso do MySQL, TYPE_PK será traduzida para int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, enquanto TYPE_STRING será varchar(255).

Você pode adicionar algumas constraints ao usar tipos abstratos. No exemplo acima, NOT NULL é adicionado a Schema::TYPE_STRING para especificar que a coluna não pode ser nula.

Observação: O mapeamento entre tipos abstratos e tipos físicos é especificado pela propriedade $typeMap em cada classe QueryBuilder.

Migrações Transacionais

Ao realizar migrações de dados complexas, é importante assegurar que cada migração terá sucesso ou falhará por completo para que o banco não perca sua integridade e consistência. Para atingir este objetivo, recomenda-se que você encapsule suas operações de banco de dados de cada migração em uma transação.

Um jeito mais fácil de implementar uma migração transacional é colocar o seu código de migração nos métodos safeUp() e safeDown(). Estes métodos diferem de up() e down() porque eles estão implicitamente encapsulados em uma transação. Como resultado, se qualquer operação nestes métodos falhar, todas as operações anteriores sofrerão roll back automaticamente.

No exemplo a seguir, além de criar a tabela noticias nós também inserimos um registro inicial a esta tabela.


use yii\db\Schema;
use yii\db\Migration;

class m150101_185401_criar_tabela_noticias extends Migration
{
    public function safeUp()
    {
        $this->createTable('noticias', [
            'id' => 'pk',
            'titulo' => Schema::TYPE_STRING . ' NOT NULL',
            'conteudo' => Schema::TYPE_TEXT,
        ]);
        
        $this->insert('noticias', [
            'titulo' => 'título 1',
            'conteudo' => 'conteúdo 1',
        ]);
    }

    public function safeDown()
    {
        $this->delete('noticias', ['id' => 1]);
        $this->dropTable('noticias');
    }
}

Observe que normalmente quando você realiza múltiplas operações em safeUp(), você deverá reverter a ordem de execução em safeDown(). No exemplo acima nós primeiramente criamos a tabela e depois inserimos uma túpla em safeUp(); enquanto em safeDown() nós primeiramente apagamos o registro e depois eliminamos a tabela.

Observação: Nem todos os SGBDs suportam transações. E algumas requisições de banco não podem ser encapsuladas em uma transação. Para alguns exemplos, referir a commit implícito. Se este for o caso, implemente os métodos up() e down().

Métodos de Acesso ao Banco de Dados

A classe base yii\db\Migration entrega vários métodos que facilitam o acesso e a manipulação de bancos de dados. Você deve achar que estes métodos são nomeados similarmente a métodos DAO encontrados na classe yii\db\Command. Por exemplo, o método yii\db\Migration::createTable() permite que você crie uma nova tabela assim como yii\db\Command::createTable() o faz.

O benefício ao usar os métodos encontrados em yii\db\Migration é que você não precisa criar explícitamente instancias de yii\db\Command e a execução de cada método automaticamente exibirá mensagens úteis que dirão a você quais operações estão sendo feitas e quanto tempo elas estão durando.

Abaixo está uma lista de todos estes métodos de acesso ao banco de dados:

Observação: yii\db\Migration não possui um método de consulta ao banco de dados. Isto porque você normalmente não precisará exibir informações extras ao recuperar informações de um banco de dados. E além disso você pode usar o poderoso Query Builder para construir e executar consultas complexas.

Aplicando Migrações

Para atualizar um banco de dados para a sua estrutura mais atual, você deve aplicar todas as migrações disponíveis usando o seguinte comando:

yii migrate

Este comando listará todas as migrações que não foram alicadas até agora. Se você confirmar que deseja aplicar estas migrações, cada nova classe de migração executará os métodos up() ou safeUp() um após o outro, na ordem relacionada à data marcada em seus nomes. Se qualquer uma das migrações falhar, o comando terminará sem aplicar o resto das migrações.

Para cada migração aplicada com sucesso, o comando inserirá um registro numa tabela no banco de dados chamada migration para registrar uma aplicação de migração. Isto permitirá que a ferramenta de migração identifique quais migrações foram aplicadas e quais não foram.

Observação: Esta ferramenta de migração automaticamente criará a tabela migration no banco de dados especificado pela opção do comando db. Por padrão, o banco de dados é especificado por db em Componentes de Aplicação.

Eventualmente, você desejará aplicar apenas uma ou algumas migrações, em vez de todas as disponíveis. Você pode fazê-lo especificando o número de migrações que deseja aplicar ao executar o comando. Por exemplo, o comando a seguir tentará aplicar as próximas 3 migrações disponíveis:

yii migrate 3

Você também pode especificar para qual migração em particular o banco de dados deve ser migrado usando o comando migrate/to em um dos formatos seguintes:

yii migrate/to 150101_185401                        # usando a marcação de data para especificar a migração
yii migrate/to "2015-01-01 18:54:01"                # usando uma string que pode ser analisada por strtotime()
yii migrate/to m150101_185401_criar_tabela_noticias # usando o nome completo
yii migrate/to 1392853618                           # usando uma marcação de data no estilo UNIX

Se existirem migrações mais recentes do que a especificada, elas serão todas aplicadas antes da migração definida.

Se a migração especificada já tiver sido aplicada, qualquer migração posterior já aplicada será revertida.

Revertendo Migrações

Para reverter uma ou múltiplas migrações que tenham sido aplicadas antes, você pode executar o seguinte comando:

yii migrate/down     # reverter a última migração aplicada
yii migrate/down 3   # reverter as 3 últimas migrações aplicadas

Observação: Nem todas as migrações são reversíveis. Tentar reverter tais migrações causará um erro que cancelará todo o processo de reversão.

Refazendo Migrações

Refazer as migrações significa primeiramente reverter migrações especificadas e depois aplicá-las novamente. Isto pode ser feito da seguinte maneira:

yii migrate/redo        # refazer a última migração aplicada 
yii migrate/redo 3      # refazer as 3 últimas migrações aplicadas

Observação: Se a migração não for reversível, você não poderá refazê-la.

Listando Migrações

Para listar quais migrações foram aplicadas e quais não foram, você deve usar os seguintes comandos:

yii migrate/history     # exibir as 10 últimas migrações aplicadas
yii migrate/history 5   # exibir as 5 últimas migrações aplicadas
yii migrate/history all # exibir todas as migrações aplicadas

yii migrate/new         # exibir as 10 primeiras novas migrações
yii migrate/new 5       # exibir as 5 primeiras novas migrações
yii migrate/new all     # exibir todas as novas migrações

Modificando o Histórico das Migrações

Ao invés de aplicar ou reverter migrações, pode ser que você queira apenas definir que o seu banco de dados foi atualizado para uma migração em particular. Isto normalmente acontece quando você muda manualmente o banco de dados para um estado em particular, e não deseja que as mudanças para aquela migração sejam reaplicadas posteriormente. Você pode alcançar este objetivo com o seguinte comando:

yii migrate/mark 150101_185401                        # usando a marcação de data para especificar a migração
yii migrate/mark "2015-01-01 18:54:01"                # usando uma string que pode ser analisada por strtotime()
yii migrate/mark m150101_185401_criar_tabela_noticias # usando o nome completo
yii migrate/mark 1392853618                           # usando uma marcação de data no estilo UNIX

O comando modificará a tabela migration adicionando ou deletando certos registros para indicar que o banco de dados sofreu as migrações especificadas. Nenhuma migração será aplicada ou revertida por este comando.

Customizando Migrações

Existem várias maneiras de customizar o comando de migração.

Usando Opções na Linha de Comando

O comando de migração vem com algumas opções de linha de comando que podem ser usadas para customizar o seu comportamento:

  • interactive: boolean (o padrão é true), especifica se as migrações serão executadas em modo interativo. Quando for true, ao usuário será perguntado se a execução deve continuar antes de o comando executar certas ações. Você provavelmente marcará isto para falso se o comando estiver sendo feito em algum processo em segundo plano.

  • migrationPath: string (o padrão é @app/migrations), especifica o diretório em que os arquivos das classes de migração estão. Isto pode ser especificado ou como um diretório ou como um alias. Observe que o diretório deve existir, ou o comando disparará um erro.

  • migrationTable: string (o padrão é migration), especifica o nome da tabela no banco de dados para armazenar o histórico das migrações. A tabela será automaticamente criada pelo comando caso não exista. Você também pode criá-la manualmente usando a estrutura version varchar(255) primary key, apply_time integer.

  • db: string (o padrão é db), especifica o banco de dados do componente de aplicação. Representa qual banco sofrerá as migrações usando este comando.

  • templateFile: string (o padrão é @yii/views/migration.php), especifica o caminho do arquivo de modelo que é usado para gerar um esqueleto para os arquivos das classes de migração. Isto pode ser especificado por um caminho de arquivo ou por um alias. O arquivo modelo é um script PHP em que você pode usar uma variával pré-definida $className para obter o nome da classe de migração.

O seguinte exemplo exibe como você pode usar estas opções.

Por exemplo, se nós quisermos migrar um módulo forum cujo os arquivos de migração estão localizados dentro da pasta migrations do módulo, nós podemos usar o seguinte comando:

# migrate the migrations in a forum module non-interactively
yii migrate --migrationPath=@app/modules/forum/migrations --interactive=0

Configurando o Comando Globalmente

Ao invés de fornecer opções todas as vezes que você executar o comando de migração, você pode configurá-lo de uma vez por todas na configuração da aplicação como exibido a seguir:

return [
    'controllerMap' => [
        'migrate' => [
            'class' => 'yii\console\controllers\MigrateController',
            'migrationTable' => 'backend_migration',
        ],
    ],
];

Com a configuração acima, toda a vez que você executar o comando de migração, a tabela backend_migration será usada para gravar o histórico de migração. Você não precisará mais fornecê-la através da opção migrationTable.

Migrando Múltiplos Bancos De Dados

Por padrão, as migrações são aplicadas no mesmo banco de dados especificado por db do componente de aplicação. Se você quiser que elas sejam aplicadas em um banco de dados diferente, você deve especificar na opção db como exibido a seguir:

yii migrate --db=db2

O comando acima aplicará as migrações para o banco de dados db2.

Algumas vezes pode ocorrer que você queira aplicar algumas das migrações para um banco de dados, e outras para outro banco de dados. Para atingir este objetivo, ao implementar uma classe de migração você deve especificar a ID do componente DB que a migração usará, como o seguinte:

use yii\db\Schema;
use yii\db\Migration;

class m150101_185401_criar_tabela_noticias extends Migration
{
    public function init()
    {
        $this->db = 'db2';
        parent::init();
    }
}

A migração acima será aplicada a db2, mesmo que você especifique um banco de dados diferente através da opção db. Observe que o histórico da migração continuará sendo registrado no banco especificado pela opção db. Se você tiver múltiplas migrações que usam o mesmo banco de dados, é recomenda-se criar uma classe de migração base com o código acima em init(). Então cada classe de migração poderá ser estendida desta classe base.

Dica: Apesar de definir a propriedade db, você também pode operar em diferentes bancos de dados ao criar novas conexões de banco para eles em sua classe de migração. Você então usará os métodos DAO com estas conexões para manipular diferentes bancos de dados.

Outra estratégia que você pode seguir para migrar múltiplos bancos de dados é manter as migrações para diferentes bancos de dados em diferentes pastas de migrações. Então você poderá migrar estes bancos de dados em comandos separados como os seguintes:

yii migrate --migrationPath=@app/migrations/db1 --db=db1
yii migrate --migrationPath=@app/migrations/db2 --db=db2
...

O primeiro comando aplicará as migrações em @app/migrations/db1 para o banco de dados db1, e o segundo comando aplicará as migrações em @app/migrations/db2 para db2, e assim sucessivamente.