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.

Comentários (0)

Nenhum comentário:

Postar um comentário