Rails Migrations: Gerenciando Schema de Banco de Dados com Disciplina
O Rails promove um estilo de desenvolvimento ágil e iterativo. Não esperamos acertar tudo na primeira tentativa. Em vez disso, escrevemos testes e interagimos com nossos clientes para refinar nosso entendimento conforme avançamos. Para que isso funcione, precisamos de um conjunto de práticas de apoio, e uma das mais importantes são as migrations.
As migrations permitem que o schema do banco de dados evolua constantemente conforme progredimos no desenvolvimento: adicionamos uma tabela aqui, renomeamos uma coluna ali, e assim por diante. O banco de dados muda em sincronia com o código da aplicação.
O que são Migrations?
Uma migration é simplesmente um arquivo de código Ruby no diretório db/migrate
da sua aplicação. Cada arquivo de migration tem um nome que começa com uma sequência de dígitos (tipicamente quatorze) e um underscore. Esses dígitos são a chave das migrations, pois definem a sequência na qual as migrations são aplicadas - eles são o número de versão da migration individual.
O número de versão é um timestamp UTC (Tempo Universal Coordenado) do momento em que a migration foi criada. Vejamos um exemplo do diretório db/migrate
de uma aplicação:
depot> ls db/migrate
20240610000001_create_products.rb
20240610000002_create_carts.rb
20240610000003_create_line_items.rb
20240610000004_add_quantity_to_line_items.rb
20240610000005_combine_items_in_cart.rb
20240610000006_create_orders.rb
20240610000007_add_order_id_to_line_item.rb
20240610000008_create_users.rb
Criando Migrations
Embora você possa criar esses arquivos de migration manualmente, é mais fácil (e menos propenso a erros) usar um gerador. Dois geradores criam arquivos de migration:
1. Gerador de Model
O gerador de model cria uma migration para criar a tabela associada ao model:
depot> bin/rails generate model desconto
invoke active_record
➤ create db/migrate/20240610133549_create_descontos.rb
create app/models/desconto.rb
invoke test_unit
create test/models/desconto_test.rb
create test/fixtures/descontos.yml
2. Gerador de Migration Standalone
Você também pode gerar uma migration isoladamente:
depot> bin/rails generate migration add_coluna_preco
invoke active_record
➤ create db/migrate/20240610133814_add_coluna_preco.rb
Executando Migrations
As migrations são executadas usando a task Rake db:migrate
:
depot> bin/rails db:migrate
O código de migration mantém uma tabela chamada schema_migrations
dentro de cada banco de dados Rails. Esta tabela tem apenas uma coluna, chamada version
, e terá uma linha por migration aplicada com sucesso.
Você pode forçar o banco de dados para uma versão específica fornecendo o parâmetro VERSION=
:
depot> bin/rails db:migrate VERSION=20240610000009
Anatomia de uma Migration
As migrations são subclasses da classe Rails ActiveRecord::Migration
. Quando necessário, as migrations podem conter métodos up()
e down()
:
class AdicionarEmailAoPedido < ActiveRecord::Migration
def up
add_column :pedidos, :email, :string
end
def down
remove_column :pedidos, :email
end
end
Veja como o método down()
desfaz o efeito do método up()
? Em muitos casos, o Rails pode detectar como desfazer automaticamente uma operação. O oposto de add_column()
é claramente remove_column()
. Nesses casos, simplesmente renomeando up()
para change()
, você pode eliminar a necessidade de um down()
:
class AdicionarEmailAoPedido < ActiveRecord::Migration
def change
add_column :pedidos, :email, :string
end
end
Tipos de Coluna
O terceiro parâmetro para add_column
especifica o tipo da coluna do banco de dados. O Rails tenta tornar sua aplicação independente do banco de dados subjacente usando tipos lógicos. Os tipos suportados pelas migrations são:
:binary, :boolean, :date, :datetime, :decimal,
:float, :integer, :string, :text, :time, :timestamp
Opções para Colunas
Você pode usar opções ao definir colunas em uma migration. Cada uma dessas opções é fornecida como um par chave: valor
:
add_column :pedidos, :atencao, :string, limit: 100
add_column :pedidos, :tipo_pedido, :integer
add_column :pedidos, :classe_envio, :string, null: false, default: 'prioritario'
add_column :pedidos, :valor, :decimal, precision: 8, scale: 2
Gerenciando Tabelas
Além de manipular colunas, podemos criar e remover tabelas inteiras:
class CriarHistoricoPedidos < ActiveRecord::Migration
def change
create_table :historico_pedidos do |t|
t.integer :pedido_id, null: false
t.text :observacoes
t.timestamps
end
end
end
Note que não definimos a coluna id
para nossa nova tabela. A menos que especifiquemos o contrário, as migrations Rails automaticamente adicionam uma chave primária chamada id
a todas as tabelas que criam. O método timestamps
cria as colunas created_at
e updated_at
com o tipo de dados timestamp correto.
Renomeando Colunas e Tabelas
Quando refatoramos nosso código, às vezes mudamos nomes de variáveis e colunas. As migrations Rails nos permitem fazer isso também:
# Renomear coluna
class RenomearColunaEmail < ActiveRecord::Migration
def change
rename_column :pedidos, :email, :email_cliente
end
end
# Renomear tabela
class RenomearHistoricoPedidos < ActiveRecord::Migration
def change
rename_table :historico_pedidos, :notas_pedidos
end
end
Definindo Índices
As migrations podem (e provavelmente devem) definir índices para tabelas. Por exemplo, podemos notar que uma vez que nossa aplicação tem um grande número de pedidos no banco de dados, a busca baseada no nome do cliente demora mais do que gostaríamos:
class AdicionarIndiceNomeClientePedidos < ActiveRecord::Migration
def change
add_index :pedidos, :nome
end
end
# Índice único
add_index :usuarios, :email, unique: true
# Índice composto (múltiplas colunas)
add_index :pedidos, [:data_pedido, :status]
Migrations Avançadas
Às vezes precisamos executar SQL nativo em nossas migrations. O Rails fornece o método execute()
para isso:
class AdicionarTiposPagamento < ActiveRecord::Migration[6.0]
def up
execute %{
CREATE TYPE
tipo_pagamento
AS ENUM (
'cheque',
'cartao_credito',
'ordem_compra'
)
}
end
def down
execute "DROP TYPE tipo_pagamento"
end
end
Migrations Irreversíveis
Às vezes criamos migrations que não podem ser revertidas. Por exemplo, ao alterar o tipo de uma coluna de string para integer, podemos perder dados. Nesses casos, devemos lançar uma exceção especial:
class AlterarTipoPedidoParaString < ActiveRecord::Migration
def up
change_column :pedidos, :tipo_pedido, :string, null: false
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
Conclusão
As migrations são a base para uma abordagem disciplinada e baseada em princípios de gerenciamento de configuração do schema do seu banco de dados. Elas permitem que você crie, renomeie e exclua colunas e tabelas, gerencie índices e chaves, aplique e desfaça conjuntos inteiros de mudanças, e até mesmo adicione seu próprio SQL personalizado, tudo de maneira completamente reproduzível.
Com as migrations, você pode evoluir o schema do banco de dados junto com o código da aplicação, mantendo um histórico versionado de todas as mudanças e garantindo que todos os desenvolvedores da equipe tenham exatamente a mesma estrutura de banco de dados.
Lembre-se sempre de fazer backup do banco de dados antes de executar migrations em produção, e considere o impacto de migrations irreversíveis no seu fluxo de trabalho de desenvolvimento.