0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rails】ダウンタイムなしでDBのデータを移行する

Posted at

概要

前置き

最近、サービスを止めずDBのデータを違うテーブルに移すのをよく行っています。
せっかくなので、その際の手順を記事にしました。

この記事では、ユーザーテーブルのメールアドレスの移行で話を進めます。
具体的には、ユーザーテーブルからメールアドレスを切り出し、新しく作成したメールアドレステーブルに移行する話です。

環境

  • Ruby 3.1.2
  • Rails 7.0.4
  • ridgepole 1.1.0

移行前の状態

ユーザースキーマ

下記が、移行前のユーザーテーブルのスキーマです。

create_table 'users', charset: 'utf8mb4', force: :cascade do |t|
  t.string 'name', null: false
  t.string 'email', null: false
  t.datetime 'created_at', null: false
  t.datetime 'updated_at', null: false
  t.index ['email'], name: 'index_users_on_email', unique: true
end

移行

1. ダブルライトするように変更しデプロイ

ユーザーモデルが追加/更新された際に、そのメールアドレスをユーザーメールアドレステーブルにもUPSERTされるように変更します。

以下が、追加するユーザーメールアドレススキーマです。

create_table 'user_emails', charset: 'utf8mb4', force: :cascade do |t|
  t.string 'email', null: false
  t.references :user, null: false, foreign_key: true
  t.datetime 'created_at', null: false
  t.datetime 'updated_at', null: false
  t.index ['email'], name: 'index_user_emails_on_email', unique: true
end

ユーザーモデルに、ユーザーメールアドレスモデルをUPSERTするメソッドを追加し、after_saveに設定します。

class User < ApplicationRecord
  after_save :upsert_user_email!

  has_one :user_email, dependent: :destroy

  private

  def upsert_user_email!
    if user_email.present?
      user_email.update!(email:)
    else
      create_user_email!(email:)
    end
  end
end

これで新しく追加されるユーザーや変更されたユーザーに対して、ユーザーメールアドレステーブルにもデータが作成/更新されるようになりました。

2. 古いデータを移行する

移行用のRakeTaskを実装し、古いデータに対しての移行を行ないます。
本筋から少し外れるのでここは割愛します。

3. 古いカラムを参照している箇所を切り替えるデプロイ

2でDB上のデータ移行は終わりました。
次はソースコード上でユーザーモデルのメールアドレスを参照している箇所を、ユーザーメールアドレスモデルのメールアドレスに切り替えます。

4. 古いカラムを削除する準備

いきなり古いカラムを削除すると、マイグレーションとソースコードのデプロイ間にエラーが発生する場合があります。
なのでユーザーモデルに下記記述を追加し、事前にデプロイしておきます。

class User < ApplicationRecord
  after_save :upsert_user_email!

  has_one :user_email, dependent: :destroy
  # 下記を追加
  self.ignored_columns = [:email]

  private

  def upsert_user_email!
    if user_email.present?
      user_email.update!(email:)
    else
      create_user_email!(email:)
    end
  end
end

5. 古いカラムを削除する

4のデプロイ後、数日様子を見て問題なければ古いカラムを削除します。

まとめ

移行を段階的に行うことで、何か問題が発生した場合でも元の状態に戻すことができます。
全ての移行が完了するまでに時間はかかりますが、安全に移行できるのでおすすめです。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?