概要
目的
システムのリプレースを行うことになりました。旧(現行)システムのデータベーススキーマは、度重なる継ぎ接ぎの被害を受けたり、未使用のままの列があったり等の問題があり、新システムではスキーマの見直しを行い、ゼロからマイグレーションファイルを書き直すことにしました。
しかし、旧システムのデータベースの中身は新システムにリプレース後も引き続き使用するため、新システムのデータベーススキーマのフォーマットに合わせながら移行する必要があります。そこで、旧システムのデータベースと新システムのデータベースの両方に接続しつつ、旧システムのデータを加工しながら新システムのデータベースに詰めていく簡易的な rake タスクを書くことにしました。
環境
- Ruby 2.5.1
- Rails 5.2.1
事前準備
移行スクリプトから直接現行環境のデータベースへ接続するのは恐ろしいので、dump を取ってローカル(新システムと同じマシン)にインポートしておきます。
そして、そのデータベースへの接続設定ファイルを作成します。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
をリポジトリに含めておくと良いでしょう。
移行スクリプト実装
完成例
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
以上。