# テーブルのカラムを別テーブルのカラムに移す📝

あるカラムを他のテーブルのカラムに移したときのメモです。

# やりたいこと

brands テーブルが持っている shop_id カラムを消して、shops テーブルに brand_id を持たせたい。

# 現状

brands テーブルが shop_id を持っていて、brands テーブルと shops テーブルは 1対1 の関係です。

# db/schema.rb

ActiveRecord::Schema.define(version: 20190000000000) do

  create_table "brands", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", comment: "ブランド" do |t|
    t.bigint "shop_id", null: false
    t.string "address", limit: 255, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["shop_id"], name: "index_brands_on_shop_id"
  end

  create_table "shops", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", comment: "店舗" do |t|
    t.string "name", limit: 255, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "brands", "shops", name: "fk_brands_on_shop_id", on_update: :cascade
end

# 理想

shops テーブルが brand_id を持っていて、brands テーブルと shops テーブルの 1対1 の関係は現状のままにしたい。

 # db/schema.rb

 ActiveRecord::Schema.define(version: 20190000000000) do

   create_table "brands", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", comment: "位置情報" do |t|
-    t.bigint "shop_id", null: false
     t.string "address", limit: 255, null: false
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
-    t.index ["shop_id"], name: "index_brands_on_shop_id"
   end

   create_table "shops", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", comment: "店舗" do |t|
+    t.bigint "brand_id", null: false
     t.string "name", limit: 255, null: false
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.index ["brand_id"], name: "index_shops_on_brand_id"
   end

-  add_foreign_key "brands", "shops", name: "fk_brands_on_shop_id", on_update: :cascade
+  add_foreign_key "shops", "brands", name: "fk_shops_on_brand_id", on_update: :cascade
 end

# 良くなかった解決方法

マイグレーションファイル内でカラムの追加と同時に、shops テーブルの brand_id カラムにデータを入れました。

下のコードでとりあえず、やりたかったことはできました。

# rails_app/db/migrate/20190514062242_move_brand_shop_id_column_to_shop_brand_id_column.rb

class MoveBrandShopIdColumnToShopBrandIdColumn < ActiveRecord::Migration[5.1]
  def up
    add_column :shops, :brand_id, :bigint, after: :id
    add_index :shops, :brand_id, name: :index_shops_on_brand_id
    add_foreign_key :shops, :brands, name: :fk_shops_on_brand_id, on_update: :cascade

    Shop.all.each do |shop|
      shop.update!(brand: Brand.find_by(shop: shop))
    end

    change_column :shops, :brand_id, :bigint, null: false

    remove_foreign_key :brands, :shops
    remove_index :brands, :shop_id
    remove_reference :brands, :shop
  end

  def down
    add_column :brands, :shop_id, :bigint, after: :id
    add_index :brands, :shop_id, name: :index_brands_on_shop_id
    add_foreign_key :brands, :shops, name: :fk_brands_on_shop_id, on_update: :cascade

    Brand.all.each do |brand|
      brand.update!(shop: Shop.find_by(brand: brand))
    end

    change_column :brands, :shop_id, :bigint, null: false

    remove_foreign_key :shops, :brands
    remove_index :shops, :brand_id
    remove_reference :shops, :brand
  end
end

# 何が問題だったのか

# マイグレーションが失敗してしまうかもしれない

$ rails db:migrate:reset

したときに操作しようとしたモデル(ブランド、店舗)を今後使わなくなって削除した場合、このマイグレーションが失敗してしまう。

# 時間がかかる

このマイグレーションを実行するたびに店舗とブランドの全データを更新する時間がかかる。

# 解決方法

Rake タスクでデータを移すなど、マイグレーションファイル以外でデータの変更を行うようにします。

リリースを 2 回に分ける必要があります。

# 1 回目のリリース

shops テーブルに brand_id を追加する。

# rails_app/db/migrate/20190514062242_add_brand_id_column_to_shops.rb

class AddBrandIdColumnToShops < ActiveRecord::Migration[5.1]
  def change
    add_column :shops, :brand_id, :bigint, after: :id
    add_index :shops, :brand_id, name: :index_shops_on_brand_id
    add_foreign_key :shops, :brands, name: :fk_shops_on_brand_id, on_update: :cascade
  end
end

カラムのデータを移動させる Rake タスクを作成する。

# lib/tasks/shop.rake

namespace :shop do
  desc 'Shop に brand_id 挿入'
  task add_brand_id: :environment do
    Shop.all.each do |shop|
      shop.update!(brand: Brand.find_by(shop: shop))
    end
  end
end

Rake タスクを実行する。

$ bundle exec rails shop:add_brand_id

# 2 回目のリリース

shops ターブルの brand_id カラムの null を許可しないようにする。

brands テーブルの shop_id カラムを消す。

# rails_app/db/migrate/20190514062242_move_brand_shop_id_column_to_shop_brand_id_column.rb

class MoveBrandShopIdColumnToShopBrandIdColumn < ActiveRecord::Migration[5.1]
  def change
    change_column :shops, :brand_id, :bigint, null: false

    remove_foreign_key :brands, :shops
    remove_index :brands, :shop_id
    remove_reference :brands, :shop
  end
end

# まとめ

マイグレーションファイル内にテーブルのデータを変更する処理を書いてしまうと変更対象のテーブルを削除した場合、 マイグレーションが失敗してしまいます。マイグレーションファイル内にテーブルのデータを変更するような処理は書かないようにしましょう。

# 参考🔗