Introdução a Migrations no Ruby on Rails

Este artigo abordará o recurso Migrations para modificações em bancos de dados de aplicações Ruby on Rails. Exploraremos as principais operações que podem ser realizadas, os tipos de dados e os modificadores de colunas suportados.

Mudar a estrutura do banco de dados durante o desenvolvimento de uma aplicação é uma tarefa comum que os programadores se deparam, como adicionar ao remover colunas, tabelas, entre outras atividades. Apesar de comum, essa não é uma atividade simples de realizar, principalmente quando o projeto já foi entregue ao cliente, pois qualquer descuido ao realizar operações de mudanças pode resultar em problemas sérios.

O que são Migrations

Para facilitar o processo de mudança ou evolução do banco de dados o framework Rails oferece o recurso chamado Migrations, onde é possível escrever o código de mudança do banco utilizando a linguagem Ruby ao invés de SQL. Isso evita que o desenvolvedor tenha que conhecer as minúcias de implementação de cada distribuição de banco de dados (SQLite, PostgreSQL, MySQL), ficando a cargo do framework.

Operações com Migrations

As migrations são classes que estendem de ActiveRecord::Migration. O código para de alteração do banco deve estar dentro do método change. Algumas das operações de mudança que podem ser executadas são descritas a seguir, onde os exemplos usam apenas a sintaxe Ruby para executar as modificações. Ao final de cada exemplo veja o SQL que é gerado pela Migration para o banco de dados SQLite.

Create Table

Essa estrutura cria uma tabela com o nome e campos passados por meio de um bloco (mais à frente analisaremos os tipos que podem ser usados junto a estrutura).

A sintaxe é apresentada a seguir:

create_table(:nome_da_tabela, :opções)

Na Listagem 1 temos um exemplo de criação.

Listagem 1. Criando uma migration

class Produtos < ActiveRecord::Migration def change create_table :produtos do |p| p.string :nome p.float :preco end end end

Na Listagem 2 vemos o código SQL que é gerado automaticamente.

Listagem 2. SQL Gerado

CREATE TABLE "produtos" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "nome" varchar, "preco" float );

Note que não foi necessário definir um identificador para cada produto para a tabela, pois o Rails automaticamente cria uma coluna chamada id que é auto incremento.

Drop Table

Essa estrutura exclui uma tabela de um banco de dados, conforme a sintaxe a seguir:

drop_table :nome_da_tabela

O código da Listagem 3 exclui a tabela products passada como parâmetro.

Listagem 3. Exclusão de tabela

class DropProdutos < ActiveRecord::Migration def change drop_table : products end end

Assim como na criação, o Rails gerou automaticamente um SQL equivalente a exclusão:

DROP TABLE “produtos”;

Rename Table

Para renomear uma tabela conforme os parâmetros passados usamos a sintaxe a seguir:

rename_table :nome_antigo_da_tabela, :novo_nome_da_tabela

No exemplo da Listagem 4 a tabela products é renomeada para books. Repare no código SQL que é gerado ao final.

Listagem 4. Rename table

class RenameProductsToBooks < ActiveRecord::Migration def change rename_table :products, :books end end SQL Gerado ALTER TABLE "products" RENAME TO "books";

Add Column

A sintaxe a seguir é usada para adicionar uma coluna conforme as opções passadas:

add_column :nome_da_tabela, :nome_da_nova_coluna, :tipo_da_nova_coluna, :opções

Na Listagem 5 vemos um exemplo de adição de uma coluna chamada detailscom o tipo varchar a tabela products.

Listagem 5. Adicionando coluna

class AddDetailsToProducts < ActiveRecord::Migration def change add_column :products, :details, :string end end

Assim como nas estruturas para tabelas, o Rails também gerou um código SQL:

ALTER TABLE "products" ADD "details" varchar;

Rename Column

A sintaxe a seguir altera o nome de uma coluna de acordo com as opções informadas nos parâmetros:

rename_column :nome_da_tabela, :antigo_nome_da_coluna, :novo_nome_da_coluna

Na Listagem 6 temos um exemplo que altera o nome da coluna “details” para “informations” na tabela “products”.

Listagem 6. Renomeando coluna

class RenameDetailsToInformations < ActiveRecord::Migration def change rename_column :products, :details, :informations end end

Repare o código SQL da Listagem 7, gerado pelo Rails.

Listagem 7. SQL gerado para renomear coluna

CREATE TEMPORARY TABLE "aproducts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "informations" varchar ); INSERT INTO "aproducts" ("id","name","informations") SELECT * FROM "products"; DROP TABLE "products"; CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "informations" varchar ); INSERT INTO "products" ("id","name","informations") SELECT * FROM "aproducts"; DROP TABLE "aproducts";

Note que os comandos SQL gerados para renomear a coluna da tabela no banco SQLite são bem extensos. Isso acontece por que não há, para este banco de dados, uma estrutura específica para alteração de colunas. No caso demonstrado foi criada uma tabela temporária chamada “aproducts” que foi preenchida com os dados que estavam na tabela “products”. Depois a tabela “products” foi excluída e uma nova tabela com o mesmo nome foi criada, mas dessa vez com uma coluna chamada “informations”, ao invés de “details”. Em seguida essa nova tabela foi preenchida com os dados da tabela temporária “aproducts”, que logo em seguida é removida.

Change Column

Essa instrução possibilita diversas alterações em uma coluna como, por exemplo, mudar o seu tipo, se ela pode ou não aceitar valores null, bem como alterar o valor configurado como padrão. O código a seguir mostra a sintaxe:

change_column :nome_da_tabela, :nome_da_coluna_a_ser_alterada, :novo_tipo, :novas_opções

Na Listagem 8 vemos o exemplo de alteração do tipo da coluna “informations”, que pertence a tabela “products”, de varchar para text.

Listagem 8. Change Column

class ChangeProductsInformationsToText < ActiveRecord::Migration def change change_column :products, :informations, :text end end

O SQL gerado encontra-se na Listagem 9.

Listagem 9. SQL gerado para alteração de coluna

CREATE TEMPORARY TABLE "aproducts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "informations" varchar ); INSERT INTO "aproducts" ("id","name","informations") SELECT * FROM "products"; DROP TABLE "products"; CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "informations" text ); INSERT INTO "products" ("id","name","informations") SELECT * FROM "aproducts"; DROP TABLE "aproducts";

Como o SQLite não suporta instruções específicas para mudanças em colunas, o Rails cria toda a estrutura da tabela novamente. Para que os dados não sejam perdidos o framework cria uma tabela temporária, neste caso “aproducts”, para armazenar os dados que serão migrados para a nova tabela com a estrutura modificada.

Change Column Null

Esse método é usado para alterar se uma coluna específica pode ou não aceitar valores nulos. Veja a seguir a sintaxe e na Listagem 10 um exemplo que altera uma coluna chamada “name” para não aceitar valores nulos:

change_column_null :nome_da_tabela, :nome_da_coluna, [true ou false]

Listagem 10. Alteração de coluna

class ChangeNameNullToproducts < ActiveRecord::Migration def change change_column_null :products, :name, false end end

Na Listagem 11 temos o SQL gerado pela operação da Listagem 10.

Listagem 11. SQL gerado para a operação de alteração

CREATE TEMPORARY TABLE "aproducts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, ); INSERT INTO "aproducts" ("id","name") SELECT * FROM "products"; DROP TABLE "products"; CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL ); INSERT INTO "products" ("id","name") SELECT * FROM "aproducts"; DROP TABLE "aproducts";

Change Column Default

Essa instrução é usada para inserir ou modificar um valor padrão para uma coluna. A sua sintaxe é mostrada a seguir e na Listagem 12 temos um exemplo de uso adicionando um valor padrão para uma coluna “details”:

change_column_default :nome_da_tabela, :nome_da_coluna, valor_padrao

Listagem 12. Adicionando valor padrão

class AddDefaultValueToProductsDetails < ActiveRecord::Migration def change change_column_default :products, :details, "nenhum detalhe para este produto" end end SQL Gerado CREATE TEMPORARY TABLE "aproducts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "details" text ); INSERT INTO "aproducts" ("id","name", “details”) SELECT * FROM "products"; DROP TABLE "products"; CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "details" text DEFAULT 'nenhum detalhe para este produto' ); INSERT INTO "products" ("id","name") SELECT * FROM "aproducts"; DROP TABLE "aproducts";

Remove Column

Esse comando remove uma coluna da tabela, conforme a sintaxe a seguir:

remove_column :nome_da_tabela, :nome_da_coluna_a_ser_removida

Veja na Listagem 13 um exemplo removendo a coluna informations da tabela products e o SQL gerado pela operação.

Listagem 13. Remoção de coluna

class RemoveColunmInformations < ActiveRecord::Migration def change remove_column :products, :informations end end SQL Gerado CREATE TEMPORARY TABLE "aproducts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "informations" text ); INSERT INTO "aproducts" ("id","name","informations") SELECT * FROM "products"; DROP TABLE "products"; CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar ); INSERT INTO "products" ("id","name","informations") SELECT “id”, “name” FROM "aproducts"; DROP TABLE "aproducts";

Tipos de dados suportados

Na instrução de criação de tabelas e alteração do tipo de coluna é necessário informar o tipo de dados para cada coluna. O Rails se encarrega de “traduzir” um tipo de dados da linguagem Ruby para um tipo válido no banco de dados que está sendo usado. A Tabela 1 mostra quais são os principais tipos de dados suportados, em que caso cada um deles é utilizado e qual o seu tipo correspondente no banco.

Tipo Quando é usado Correspondente no Banco de dados
string Usado para armazenar pequenas cadeias de caracteres relativamente curtas como, por exemplo, nome de uma pessoa e título de um artigo. varhar
text Comumente usado para guardar grandes quantidades de texto como, por exemplo, texto de um artigo e detalhes sobre um produto. text
float Usado para armazenar números decimais como, por exemplo, preço de um produto ou valor que uma despesa. float
integer Usado para armazenar números inteiros, isto é, sem parte fracionária como, por exemplo, a idade de uma pessoa. integer
date Usado para guardar datas como, por exemplo, a data de nascimento de um usuário, data de compra ou venda de um produto. date
datetime Semelhante ao date, mas este é utilizado para armazenar além da data o horário como, por exemplo, a data e hora do último acesso do usuário. datetime ou timestamp
time Utilizado para contar horas como, por exemplo, horário de chegada de um funcionário. time
boolean Guarda apenas valores lógicos, como true ou false. boolean
binary Armazena arquivos como, por exemplo, a foto de um usuário ou vídeo aula. binary ou blob

Tabela 1. Tipo de dados

Modificadores de Colunas

O Rails também oferece opções para definir alguns detalhes importantes para as colunas de uma tabela. A seguir analisaremos algumas dessas opções disponíveis.

Limit

A opção limit é usada para determinar o tamanho máximo do valor que pode ser inserido em uma coluna. Para tipos text ou string isso implica no limite máximo de caracteres, no caso do tipo binary ou dos tipos numéricos no número de bytes. Essa opção pode ser usada na criação de uma coluna ou mesmo quando esta esteja sendo modificada. Na Listagem 14 vemos o uso dessa instrução durante a criação de uma tabela e na alteração de uma coluna.

Listagem 14. Usando limit

class CreateProducts < ActiveRecord::Migration def change create_table :products do |t| t.string :name, :limit => 60 t.float :price end end end SQL Gerado: CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(60), "price" float );

Se for necessário alterar o valor da opção limit da coluna name criada no exemplo, basta fazer a alteração utilizando a instrução change_column, como apresentado no exemplo da Listagem 15.

Listagem 15. Uso da instrução change_column

class ChangeNameLimitProduct < ActiveRecord::Migration def change change_column :products, :name, :string, :limit => 70 end end SQL Gerado: CREATE TEMPORARY TABLE "aproducts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(60), "price" float ); INSERT INTO "aproducts" ("id","name","price") SELECT * FROM "products"; DROP TABLE "products"; CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(70), "price" float ); INSERT INTO "products" ("id","name","price") SELECT * FROM "aproducts"; DROP TABLE "aproducts";

Default

Essa opção é uma alternativa ao uso da instrução change_column_default. As duas instruções produzem o mesmo resultado, que é configurar um valor padrão para a coluna, porém, colocando um limite.

Veja a aplicação dessa estrutura no exemplo de criação da tabela books da Listagem 16, onde foi definido um valor padrão para a coluna “category”.

Listagem 16. Valor padrão usando default

class CreateBooks < ActiveRecord::Migration def change create_table :books do |t| t.string :name t.string :category, :default => 'sem categoria' end end end SQL Gerado: CREATE TABLE "books" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "category" varchar DEFAULT 'sem categoria' );

Esse valor default também pode ser alterado após a criação da tabela, por meio da instrução change_column, como mostra o exemplo da Listagem 17.

Listagem 17. Alteração de valor default

class ChangeDefaultValueToCategoryBooks < ActiveRecord::Migration def change change_column :books, :category, :string, :default => 'categoria não definida' end end SQL Gerado: CREATE TEMPORARY TABLE "abooks" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "category" varchar DEFAULT 'sem categoria' ); INSERT INTO "abooks" ("id","name","category") SELECT * FROM "books"; DROP TABLE "books"; CREATE TABLE "books" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "category" varchar DEFAULT 'categoria não definida' ); INSERT INTO "books" ("id","name","category") SELECT * FROM "abooks"; DROP TABLE "abooks";

Null

Essa instrução é semelhante a change_column_null e pode ser usada de modo alternativo, pois é possível alterar se uma coluna aceita ou não valores nulos. Veja o exemplo de utilização dessa opção no bloco de código da Listagem 18.

Listagem 18. Definição de valor nulo

class CreateProducts < ActiveRecord::Migration def change create_table :products do |t| t.string :name t.float :price, :null => false end end end SQL Gerado: CREATE TABLE "products" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "price" float NOT NULL );

Criando migrations pelo gerador

Até aqui foram analisadas as operações que podem ser executadas com as migrations, bem como detalhes sobre os tipos de dados, alguns modificadores de colunas e exemplos de mudanças que podem acontecer em um sistema real. Mas existem alguns detalhes a respeito da criação das migrations que são importantes. Elas devem estar no diretório db/migration da aplicação e o nome de cada arquivo que contém uma classe de migração deve iniciar com o padrão ano (quatro dígitos), mês (dois dígitos), dia (dois dígitos), hora (dois dígitos), minuto (dois dígitos) e segundo (dois dígitos). Por exemplo, o nome do arquivo de uma migration para criação de uma tabela de usuários poderia ser “20153011180000_create_users.rb”. Pode se tornar uma tarefa confusa e não produtiva nomear os arquivos de migrações nesse estilo, por isso o framework Rails oferece um gerador de migrações para automatizar essa tarefa, conforme as sintaxes a seguir:

rails generate migration NomeDaMigration ou ainda rails g migration NomeDaMigration

A seguir temos o exemplo de uma nova migration para criação de uma tabela de clientes (customers), onde o mesmo deve ser executado no terminal, dentro do projeto para o qual se deseja criar a migração:

rails generate migration CreateCustomers

O comando imprime a seguinte saída no terminal:

invoke active_record create db/migrate/20151130222636_create_customers.rb

Note que o gerador criou o arquivo no diretório db/migrate obedecendo a ordem de nomenclatura que já foi abordada. O arquivo 20151130222636_create_customers.rb foi criado com o seguinte conteúdo da Listagem 19.

Listagem 19. Conteúdo da migration

class CreateCustomers < ActiveRecord::Migration def change create_table :customers do |t| end end end

O gerador construiu a maior parte do código que é necessário para a migração de criação da tabela, bastando agora acrescentar as colunas dentro do bloco create_table com seus respectivos tipos e modificadores. Mas essa parte também pode ser feita pelos geradores.

Vamos começar com a construção de uma migration para a criação de uma tabela de usuários (user) com as colunas nome (name), idade (age), senha (password), endereço (address). Note que o nome dela deve iniciar com “Create” seguido do nome da tabela, bem como a lista de colunas com seus respectivos tipos. Para isso use o código a seguir:

rails generate migration CreateUsers name:string age:integer password:string address:text

O comando gera a seguinte saída:

invoke active_record create db/migrate/20151130230741_create_users.rb

O arquivo 20151130230741_create_users.rb foi gerado com o conteúdo da Listagem 20.

Listagem 20. Código gerado automaticamente

class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.integer :age t.string :password t.text :address end end end

Veja que os campos especificados na linha de comando foram automaticamente adicionados ao bloco create_table com os tipos informados.

O framework também oferece este mesmo recurso para geração automática do código no caso de adição ou remoção de colunas. Para a adição o nome da migração deve seguir o padrão “AddCampoToTabela” como, por exemplo, “AddPhoneToUsers”. Já para a remoção podemos usar “RemoveCampoToTabela” como, por exemplo, “RemoveAgeToUsers”. Nos dois casos, o nome das migrações deve ser seguido pelo nome dos atributos e seus tipos, como no código a seguir:

rails generate migration AddEmailToUsers email:String

O comando gerou o arquivo 20151130232514_add_email_to_users.rb com o seguinte conteúdo:

class AddEmailToUsers < ActiveRecord::Migration def change add_column :users, :email, :string end end

O exemplo a seguir mostra como funciona o comando para remoção de uma coluna:

rails generate migration RemoveEmailToUsers e-mail

A instrução teve como resultado o arquivo 20151130232950_remove_email_to_users.rb com o seguinte conteúdo para remoção da coluna e-mail:

class RemoveEmailToUsers < ActiveRecord::Migration def change remove_column :users, :email, :string end end

Executando uma migration

Depois de criar uma migration, para que ela faça efetivamente as mudanças no banco de dados, é necessário executá-la. Isso é feito com a ajuda do rake usando o seguinte comando:

rake db:migrate

Esse comando vai “rodar” todas as migrações que estão armazenadas no diretório db/migrate/ e que ainda não foram executadas no banco de dados. Esta execução obedece a ordem de criação dos arquivos de migração.

Também é possível executar uma migração específica, para isso basta adicionar a opção VERSION no comando rake, seguido do número da migração que se deseja executar, como no exemplo a seguir:

rake db:migrate VERSION= 20151130232950

Vimos nos exemplos apresentados como o Rails cuida de forma muito ágil das modificações que podem ser feitas nos bancos. O código usado no framework é bem menor em relação ao banco SQLite.

Quando trabalhar com esse recurso, fique atento a identificação de erros e não deixe de modelar para minimizar os impactos das mudanças. Além disso, não recomendamos usar migrations para bancos grandes, devido às restrições de espaço.

Referências

Rails Guide
http://guides.rubyonrails.org/active_record_migrations.html

API Ruby on Rails
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

Tutorials Point
http://www.tutorialspoint.com/ruby-on-rails/rails-migrations.htm

Artigos relacionados