Ruby
Rails
RubyOnRails

[Ruby on Rails] 旧システムから新システムへのデータマイグレーション

概要

目的

システムのリプレースを行うことになりました。旧(現行)システムのデータベーススキーマは、度重なる継ぎ接ぎの被害を受けたり、未使用のままの列があったり等の問題があり、新システムではスキーマの見直しを行い、ゼロからマイグレーションファイルを書き直すことにしました。

しかし、旧システムのデータベースの中身は新システムにリプレース後も引き続き使用するため、新システムのデータベーススキーマのフォーマットに合わせながら移行する必要があります。そこで、旧システムのデータベースと新システムのデータベースの両方に接続しつつ、旧システムのデータを加工しながら新システムのデータベースに詰めていく簡易的な rake タスクを書くことにしました。

環境

  • Ruby 2.5.1
  • Rails 5.2.1

事前準備

移行スクリプトから直接現行環境のデータベースへ接続するのは恐ろしいので、dump を取ってローカル(新システムと同じマシン)にインポートしておきます。

そして、そのデータベースへの接続設定ファイルを作成します。database.yml と同様です。

config/old_system_database.yml(例)
adapter: mysql2
encoding: utf8
database: appname_production
host: 127.0.0.1
username: root
password:
port: 3306

git 管理している場合は、 database.yml でよくやるように、 old_system_database.yml.gitignore しておいて、 old_system_database.yml.sample をリポジトリに含めておくと良いでしょう。

移行スクリプト実装

完成例

lib/tasks/migrate_from_old_system.rake
desc 'Migrate from old system'

task migrate_from_old_system: :environment do
  class OldSystemModel < ApplicationRecord
    establish_connection(YAML.safe_load(IO.read('config/old_system_database.yml')))
  end

  # そのまま移行して良いテーブルの移行例
  class OldPicture < OldSystemModel
    self.table_name = :pictures
  end
  OldPicture.find_each { |old| Picture.create!(old.attributes) }

  # テーブル名が変わるテーブルの移行例
  class OldCameraman < OldSystemModel
    self.table_name = :cameramen
  end
  OldCameraman.find_each { |old| Photographer.create!(old.attributes) }

  # 列の内容が変わるテーブルの移行例
  class OldUser < OldSystemModel
    self.table_name = :users
  end
  OldUser.find_each do |old|
    attributes = old.attributes
    attributes.delete('avatar')
    attributes['photographer_id'] = attributes.delete('cameraman_id')
    User.create!(attributes)
  end
end

解説

旧データベースと接続するモデルクラス

ApplicationRecord を継承したモデルクラスは、デフォルトで config/database.yml に記述されているデータベース、なければ ENV['DATABASE_URL'] を見に行きますが、 ActiveRecord::Base.establish_connection に接続情報を与えることで、任意のデータベースと接続するモデルクラスを作ることができます。ここで、事前準備で作成した old_system_database.yml を使います。

旧データベースと接続する全てのモデルクラスで establish_connection(YAML.safe_load(IO.read('config/old_system_database.yml'))) と書くのはしんどいので、ベースクラスを作ることにします。ここでは、旧システムのデータベースと接続するモデルクラスは OldSystemModel クラスを継承することにします。サブクラス側では、 ActiveRecord::Base.table_name を呼んで旧システムでのテーブル名を指定します。

  class OldSystemModel < ApplicationRecord
    establish_connection(YAML.safe_load(IO.read('config/old_system_database.yml')))
  end

  class OldPicture < OldSystemModel
    self.table_name = :pictures
  end

そのまま移行して良いテーブルの移行例

まずは最も簡単な、テーブル名も列の内容も変わっておらず、何も気にせずそのまま移行して良い場合のスクリプト例です。 pictures テーブルには何の変更も行われなかったので、id , created_at , updated_at も含む全ての属性をまとめて移行します。全ての属性は attributes メソッドで取得することができます。

  class OldPicture < OldSystemModel
    self.table_name = :pictures
  end
  OldPicture.find_each { |old| Picture.create!(old.attributes) }

テーブル名が変わるテーブルの移行例

「カメラマン」という呼び名をやめて「フォトグラファー」という呼び名を使いましょう、という話になったのでテーブル名も cameramen から photographers に変えることになった場合です。旧システムと接続するモデルクラスの table_name 次第なので影響は受けません。

  class OldCameraman < OldSystemModel
    self.table_name = :cameramen
  end
  OldCameraman.find_each { |old| Photographer.create!(old.attributes) }

列の内容が変わるテーブルの移行例

旧システムではユーザーのアバター画像のアップローダーに carrierwave を使っていたため、 users テーブルの avatar という列にファイル名を保存していましたが、新システムでは Rails 5.2 系の ActiveStorage が使えるため、 avatar 列は削除することにしました。

また、上記で「カメラマン」という呼び名をやめて「フォトグラファー」という呼び名を使うことに決まった関係で、 users テーブルの持つ外部キーの名前が cameraman_id から photographer_id に変わったので属性の名前を差し替えます。

このような形で、Ruby で属性の内容をゴリゴリ書き換えてやることで任意のフォーマットに移行することができます。

  # 列の内容が変わるテーブルの移行例
  class OldUser < OldSystemModel
    self.table_name = :users
  end
  OldUser.find_each do |old|
    attributes = old.attributes
    attributes.delete('avatar')
    attributes['photographer_id'] = attributes.delete('cameraman_id')
    User.create!(attributes)
  end

以上。