Migrações de dados em Ruby on Rails

jamalcrf

Lucas Araújo

Posted on March 10, 2022

Migrações de dados em Ruby on Rails

Desde o início dessa minha caminhada como desenvolvedor Ruby on Rails, eu venho aprendendo determinadas técnicas e ferramentas que me auxiliam no meu dia a dia. Uma vez no meu trabalho, ocorreu a necessidade de alterar os dados reais no banco de dados de produção. Eu pensei rapidamente e a primeira opção óbvia que vem à mente é usar uma migração do Rails, porém conversando com colegas da minha equipe, fui convencido de que isso não seria uma boa prática e a partir daí busquei entender um pouco mais sobre como migrar dados de maneira correta e mais segura utilizando Ruby on Rails.

Utilizando o sistema de migrações do Rails

Observando o Rails Guide, for Active Record Migration a primeira seção da documentação começa dizendo:

"As migrações são um recurso do Active Record que permite evoluir seu esquema de banco de dados ao longo do tempo. Em vez de escrever modificações de esquema em SQL puro, as migrações permitem que você use uma DSL Ruby para descrever as alterações em suas tabelas."

Claramente, a palavra "dados" em nenhum momento foi citada no parágrafo acima, isso porque, por definição, as migrações do Rails devem ser usadas apenas para alterações do schema e não para alterações de dados reais no banco de dados.

Podemos criar um cenário básico para entendermos um pouco mais sobre isso. Digamos que nós precisamos alterar o estado padrão de um blog qualquer, por exemplo, sendo assim criaríamos um arquivo de migração como este abaixo:

rails generate migration ChangeDefaultState
  invoke  active_record
  create    db/migrate/20220305055147_change_default_state.rb

Enter fullscreen mode Exit fullscreen mode

Nesse arquivo, faríamos a seguinte alteração como essa:

class ChangeDefaultState < ActiveRecord::Migration[5.1]
  def up
    Post.where(state: "Initial").update_all(state: "Active")
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end
Enter fullscreen mode Exit fullscreen mode

Feito isso, está tudo certo! É só executar a migração e esquecer dela... Meses depois aparece uma demanda no seu sistema, onde nós precisamos alterar o nome da tabela posts para articles. Até então é uma tarefa simples de ser executada. Alterando o nome da tabela, renomeando o modelo e pronto, só mandar para produção. Pois bem, agora nós não temos mais um modelo chamado Post. Na próxima vez que você precisar configurar um ambiente de desenvolvimento, você cria seu banco de dados, executa suas migrações e obtém uma falha no seu console: Rails não sabe o que é Post:

$ rails db:migrate
== 20220305056456 AddDefaultState: migrating ==================================
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
uninitialized constant AddDefaultState::Post
/Users/lucas/Projects/codes/serious/db/migrate/20220305055147_change_default_state.rb:3:in `change'
Enter fullscreen mode Exit fullscreen mode

A solução para esse tipo de problema, de fato, é rápida: você precisa renomear o modelo na sua migração. Entretanto esse padrão não é a melhor abordagem. Resumidamente,as migrações ficam desatualizadas rapidamente e você não tem uma maneira eficiente de descobrir, além do mais não estaria seguindo a convenção do Rails: os arquivos db/migrate devem migrar apenas as estrutura do banco de dados e não os registros do banco de dados.

Outro detalhe é que o sistema agora depende da conclusão da migração de dados. Isso pode não ser um problema quando seu aplicativo é novo e seu banco de dados é pequeno. Mas e os grandes bancos de dados com milhões de registros? Sua implantação agora terá que esperar que a manipulação de dados seja concluída e isso é apenas pedir problemas, com possíveis migrações suspensas ou com falha. Dito isso, pode-se dizer que incluir os códigos de migração de dados no db:/migrate é uma prática "anti-padrão".

Utilizando Rake Tasks para migrar dados

Um método mais confiável, acessível e eficiente é criar Rake Tasks para migrações de dados. Escrevendo um pouco mais de código nós podemos resolver esse tipo de problema.

namespace :data do
  task :migrations do
    Rake.application.in_namespace(:data) do |namespace|
      namespace.tasks.each do |t|
        next if t.name == "data:migrations"
        puts "Invoking #{t.name}:"
        t.invoke
      end
    end
  end

  task change_default_state: :environment do
    puts "Changing default state for posts"
    Post.where(state: "Initial").update_all(state: "Active")
    puts "Changed default state for posts"
  end
Enter fullscreen mode Exit fullscreen mode

A primeira Task ( rake data:migrations) executará todas as tarefas no data namespace excluindo ela mesma. Isso pode ser um pouco arriscado! então você deve se certificar de que todas as Tasks nesse namespace sejam idempotentes, ou seja, realizadas com sucesso independente do número de vezes que é executada. Você deseja poder executar rake data:migrations o quanto quiser sem arriscar a perda de dados.

A ressalva que temos com essa abordagem é que ela não resolve o problema que mencionamos no primeiro padrão: à medida que o sistema evolui, as Tasks ficarão desatualizadas.

A melhor maneira é criar uma classe adicional. Podemos chamá-loChangeDefaultStateForPosts. Será um objeto Ruby simples que executará a migração de dados. Isso nos ajudará a adicionar cobertura de testes para ele.

class ChangeDefaultStateForPosts
  def self.call
    Post.where(state: "Initial").update_all(state: "Active")
  end
end
Enter fullscreen mode Exit fullscreen mode

Agora que temos uma classe simples e com isso podemos escrever um arquivo de testes para ela.

RSpec.describe ChangeDefaultStateForPosts do
  describe "ChangeDefaultStateForPosts.call" do
    let!(:post) do
      Post.create(state: state)
    end

    context "when post is in 'Initial' state" do
      let(:state) { "Initial" }

      it "changes the state of the Initial" do
        expect do
          ChangeDefaultStateForPosts.call
        end.to change(post, :state)
      end
    end

    context "when post is in another state" do
      let(:state) { "Published" }

      it "doesn't change the state" do
        expect do
          ChangeDefaultStateForPosts.call
        end.not_to change(post, :state)
      end
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

Sendo assim estamos seguros pois a nossa Task sobre a migração de dados é protegidas pelo nosso arquivo de teste. Quando alguém tentar renomear a tabela posts para articles, receberá uma falha no teste. Isso os forçará a atualizar o arquivo de migração de dados.

Caso essa opção seja válida e queira implementar no seu projeto, é bastante recomendado documentar todo esse processo.

Migrações de dados com a data_migrate

Como todo desenvolvedor Rails, provavelmente, em algum momento desse artigo, você chegou a se perguntar se talvez existisse uma Gem para fazer esse processo de migração de dados... SIM, nós podemos usar a data_migrate para todas as nossas migrações de dados. Assim como o Rails com a tabela schema_migrations, a data_migrate usa uma tabela especifica chamada data_migrations para acompanhar as migrações novas e as antigas.

Para o problema que estamos tentando resolver, você pode criar uma nova migração de dados como esta:

rails generate data_migration change_default_state_for_posts

Isso adicionará uma nova migração de dados ao diretório db/data. Você precisará definir os métodos up e down:

class ChangeDefaultStateForPosts < ActiveRecord::Migration[5.1]
  def up
    Post.where(state: "Initial").update_all(state: "Active")
  end

  def down
    Post.where(state: "Active").update_all(state: "Initial")
  end
end
Enter fullscreen mode Exit fullscreen mode

E, em seguida, execute e verifique o status com comandos como estes:

rake data:migrate
rake db:migrate:with_data
rake db:rollback:with_data
rake db:migrate:status:with_data

Enter fullscreen mode Exit fullscreen mode

Acredito que a melhor maneira de resolver esse problema é usar a Gem data_migrate. Você escreverá menos código, manterá todas as migrações de dados em um diretório db/data e terá uma boa maneira de acompanhar as alterações de dados.

💖 💪 🙅 🚩
jamalcrf
Lucas Araújo

Posted on March 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related