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 “details” com 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