Rails: herokuでstagingからproductionにデータコピーする方法

herokuは起動に30秒かかってもOKならdyno=1で無料なので、staging環境が気軽に作れる。

qiita.com

stagingで確認したデータをそのままproductionに移したい。 こんなかんじでできる。

 def self.copy_to_production(object)

    ...   
    #DBを本番に切り替え
    object.class.establish_connection :adapter => 'postgresql',
      :encoding => 'unicode',
      :database => ENV["PRODUCTION_DB_NAME"],
      :pool => '5',
      :username => ENV["PRODUCTION_DB_USERNAME"],
      :password => ENV["PRODUCTION_DB_PASSWORD"],
      :host => ENV["PRODUCTION_DB_HOST"],
      :port => ENV["PRODUCTION_DB_PORT"]
    ...

    #本番にデータを追加
    attrs = object.attributes
    attrs.delete("id")
    attrs.delete_if{|item| item.include?("_id")} #外部キーは環境で異なるので削除
    new_object = object.class.create(attrs)
    new_object.save
  end

objectはActiveRecord::Baseのサブクラス(RailsのModel)のインスタンス

establish_connectionで別DB(productionのDB)に繋ぎ直して、そっちでcreateするとそっちにcommitされる。

connectionの設定情報はherokuのherokupostgresの管理画面から確認できる。

f:id:yuma_nishizaki:20150723235237p:plain f:id:yuma_nishizaki:20150723235227p:plain

connectionを元に戻す

で、これだけだとproductionのDBに繋げたまんまで、stagingに戻ってこれない。 (正確には、コピーしたオブジェクトのクラス単位(ActiveRecord::Baseのサブクラス単位)で別DBにつながるみたいなんで、例えばコピーしたクラスがUserクラスだったとすると、このままでもそれ以外の例えばBookクラスがあったとすると、これはstagingにつながったままになっている。)

Herokuでいうと、

heroku run rails c -a staging

して、上のメソッド使ってコピーして、rails cを終了させれば、DBの接続も切られて、新たにrails cしたときには再びデフォルトのstagingの方につながるので、この用途だけで使ってる分には問題ないっちゃないけど、 でもアプリケーションの中のロジックとして使うならちゃんと明示的に戻してやる必要あり。

  def self.copy_to_production(object)
    begin
      #DBを本番に切り替え
      object.class.establish_connection :adapter => 'postgresql',
        :encoding => 'unicode',
        :database => ENV["PRODUCTION_DB_NAME"],
        :pool => '5',
        :username => ENV["PRODUCTION_DB_USERNAME"],
        :password => ENV["PRODUCTION_DB_PASSWORD"],
        :host => ENV["PRODUCTION_DB_HOST"],
        :port => ENV["PRODUCTION_DB_PORT"]


      #本番にデータを追加
      attrs = object.attributes
      attrs.delete("id")
      attrs.delete_if{|item| item.include?("_id")} #外部キーは環境で異なるので削除
      new_object = object.class.create(attrs)
      new_object.save
    ensure
      object.class.establish_connection(ENV["DATABASE_URL"]) #元に戻す
    end
  end 

begin 〜 ensureで書いてやればOK。 establish_connectionはDB情報の引数での渡し方いろいろあって、

establish_connection (ActiveRecord::ConnectionHandling) - APIdock

URLでも渡せるし、上でやったようにhashで一個一個渡してもOK。

herokuだとENV["DATABASE_URL"]にデフォルトで必要情報はいってるから、戻すときはこれでOK。

動的にconfigurationになにか追加されている可能性があるため、元々の設定を取得しておいて、あとからそれを戻すほうが正解。

    original_config = ActiveRecord::Base.configurations[Rails.env] ||
                      Rails.application.config.database_configuration[Rails.env]   
    begin
    ...
    ensure
      object.class.establish_connection(original_config) #元に戻す
    end

参考:

When connecting to a second database, take care not to overwrite existing connections - makandropedia

これによると、begin 〜 ensureの書き方だとtransactionを絡めようと思うとうまくいかないらしく、サブクラス作って

class ReadDatabaseConnection < ActiveRecord::Base
  def self.abstract_class?
    true # So it gets its own connection
  end
end

を実装したうえで、このサブクラスに対してestablish_connection呼んでやれば別connectionが貼られるらしい。 transaction絡める場合は注意。

paperclipでS3に画像保存している場合

あとで書こう。