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

# 概要

## 目的

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

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

## 環境

- Ruby 2.5.1
- Rails 5.2.1

## 事前準備

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

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

```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` をリポジトリに含めておくと良いでしょう。


# 移行スクリプト実装

## 完成例

```ruby: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` を呼んで旧システムでのテーブル名を指定します。

```ruby
  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` メソッドで取得することができます。

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

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

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

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

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

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

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

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

```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
```

以上。